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
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
376 if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
377
378
379
380 if (debug) {
381 System.err.println("current < start");
382 }
383
384 return false;
385 }
386
387 Calendar candidate = getCandidateStartTime(myCurrent);
388
389
390
391 while ((candidate.getTime().getTime() + duration.getInterval()) >
392 myCurrent.getTime().getTime()) {
393
394 if (candidateIsInRecurrence(candidate, debug)) {
395 return true;
396 }
397
398
399
400 candidate.add(Calendar.SECOND, -1);
401
402
403
404 if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
405 if (debug) {
406 System.err.println("No candidates after dtStart");
407 }
408
409 return false;
410 }
411
412 candidate = getCandidateStartTime(candidate);
413 }
414
415 if (debug) {
416 System.err.println("No matching candidates");
417 }
418
419 return false;
420 }
421
422
425 public void setByDay(DayAndPosition[] b) {
426 if (b == null) {
427 byDay = null;
428
429 return;
430 }
431
432 byDay = new DayAndPosition[b.length];
433
434
438 for (int i = 0; i < b.length; i++) {
439 byDay[i] = (DayAndPosition)b[i].clone();
440 }
441 }
442
443
446 public void setByMonth(int[] b) {
447 if (b == null) {
448 byMonth = null;
449
450 return;
451 }
452
453 byMonth = new int[b.length];
454
455 System.arraycopy(b, 0, byMonth, 0, b.length);
456 }
457
458
461 public void setByMonthDay(int[] b) {
462 if (b == null) {
463 byMonthDay = null;
464
465 return;
466 }
467
468 byMonthDay = new int[b.length];
469
470 System.arraycopy(b, 0, byMonthDay, 0, b.length);
471 }
472
473
476 public void setByWeekNo(int[] b) {
477 if (b == null) {
478 byWeekNo = null;
479
480 return;
481 }
482
483 byWeekNo = new int[b.length];
484
485 System.arraycopy(b, 0, byWeekNo, 0, b.length);
486 }
487
488
491 public void setByYearDay(int[] b) {
492 if (b == null) {
493 byYearDay = null;
494
495 return;
496 }
497
498 byYearDay = new int[b.length];
499
500 System.arraycopy(b, 0, byYearDay, 0, b.length);
501 }
502
503
506 public void setDtEnd(Calendar end) {
507 Calendar tempEnd = (Calendar)end.clone();
508
509 tempEnd.clear(Calendar.ZONE_OFFSET);
510 tempEnd.clear(Calendar.DST_OFFSET);
511 tempEnd.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
512 duration.setInterval(
513 tempEnd.getTime().getTime() - dtStart.getTime().getTime());
514 }
515
516
519 public void setDtStart(Calendar start) {
520 int oldStart;
521
522 if (dtStart != null) {
523 oldStart = dtStart.getFirstDayOfWeek();
524 }
525 else {
526 oldStart = Calendar.MONDAY;
527 }
528
529 if (start == null) {
530 dtStart = CalendarFactoryUtil.getCalendar(
531 TimeZoneUtil.getTimeZone(StringPool.UTC));
532
533 dtStart.setTime(new Date(0L));
534 }
535 else {
536 dtStart = (Calendar)start.clone();
537
538 dtStart.clear(Calendar.ZONE_OFFSET);
539 dtStart.clear(Calendar.DST_OFFSET);
540 dtStart.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
541 }
542
543 dtStart.setMinimalDaysInFirstWeek(4);
544 dtStart.setFirstDayOfWeek(oldStart);
545 }
546
547
550 public void setDuration(Duration d) {
551 duration = (Duration)d.clone();
552 }
553
554
557 public void setFrequency(int freq) {
558 if ((frequency != DAILY) && (frequency != WEEKLY) &&
559 (frequency != MONTHLY) && (frequency != YEARLY) &&
560 (frequency != NO_RECURRENCE)) {
561
562 throw new IllegalArgumentException("Invalid frequency");
563 }
564
565 frequency = freq;
566 }
567
568
571 public void setInterval(int intr) {
572 interval = (intr > 0) ? intr : 1;
573 }
574
575
578 public void setOccurrence(int occur) {
579 occurrence = occur;
580 }
581
582
585 public void setUntil(Calendar u) {
586 if (u == null) {
587 until = null;
588
589 return;
590 }
591
592 until = (Calendar)u.clone();
593
594 until.clear(Calendar.ZONE_OFFSET);
595 until.clear(Calendar.DST_OFFSET);
596 until.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
597 }
598
599
602 public void setWeekStart(int weekstart) {
603 dtStart.setFirstDayOfWeek(weekstart);
604 }
605
606
611 @Override
612 public String toString() {
613 StringBundler sb = new StringBundler();
614
615 sb.append(getClass().getName());
616 sb.append("[dtStart=");
617 sb.append((dtStart != null) ? dtStart.toString() : "null");
618 sb.append(",duration=");
619 sb.append((duration != null) ? duration.toString() : "null");
620 sb.append(",frequency=");
621 sb.append(frequency);
622 sb.append(",interval=");
623 sb.append(interval);
624 sb.append(",until=");
625 sb.append((until != null) ? until.toString() : "null");
626 sb.append(",byDay=");
627
628 if (byDay == null) {
629 sb.append("null");
630 }
631 else {
632 sb.append("[");
633
634 for (int i = 0; i < byDay.length; i++) {
635 if (i != 0) {
636 sb.append(",");
637 }
638
639 if (byDay[i] != null) {
640 sb.append(byDay[i].toString());
641 }
642 else {
643 sb.append("null");
644 }
645 }
646
647 sb.append("]");
648 }
649
650 sb.append(",byMonthDay=");
651 sb.append(stringizeIntArray(byMonthDay));
652 sb.append(",byYearDay=");
653 sb.append(stringizeIntArray(byYearDay));
654 sb.append(",byWeekNo=");
655 sb.append(stringizeIntArray(byWeekNo));
656 sb.append(",byMonth=");
657 sb.append(stringizeIntArray(byMonth));
658 sb.append(']');
659
660 return sb.toString();
661 }
662
663
668 protected static long getDayNumber(Calendar cal) {
669 Calendar tempCal = (Calendar)cal.clone();
670
671
672
673 tempCal.set(Calendar.MILLISECOND, 0);
674 tempCal.set(Calendar.SECOND, 0);
675 tempCal.set(Calendar.MINUTE, 0);
676 tempCal.set(Calendar.HOUR_OF_DAY, 0);
677
678 return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
679 }
680
681
686 protected static long getMonthNumber(Calendar cal) {
687 return
688 ((cal.get(Calendar.YEAR) - 1970) * 12L) +
689 (cal.get(Calendar.MONTH) - Calendar.JANUARY);
690 }
691
692
697 protected static long getWeekNumber(Calendar cal) {
698 Calendar tempCal = (Calendar)cal.clone();
699
700
701
702 tempCal.set(Calendar.MILLISECOND, 0);
703 tempCal.set(Calendar.SECOND, 0);
704 tempCal.set(Calendar.MINUTE, 0);
705 tempCal.set(Calendar.HOUR_OF_DAY, 0);
706
707
708
709 int delta =
710 tempCal.getFirstDayOfWeek() - tempCal.get(Calendar.DAY_OF_WEEK);
711
712 if (delta > 0) {
713 delta -= 7;
714 }
715
716
717
718
719
720
721 long weekEpoch =
722 (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24L * 60 * 60 *
723 1000L;
724
725 return
726 (tempCal.getTime().getTime() - weekEpoch) /
727 (7 * 24 * 60 * 60 * 1000);
728 }
729
730
733 protected static void reduce_constant_length_field(
734 int field, Calendar start, Calendar candidate) {
735
736 if ((start.getMaximum(field) != start.getLeastMaximum(field)) ||
737 (start.getMinimum(field) != start.getGreatestMinimum(field))) {
738
739 throw new IllegalArgumentException("Not a constant length field");
740 }
741
742 int fieldLength =
743 (start.getMaximum(field) - start.getMinimum(field) + 1);
744 int delta = start.get(field) - candidate.get(field);
745
746 if (delta > 0) {
747 delta -= fieldLength;
748 }
749
750 candidate.add(field, delta);
751 }
752
753
756 protected static void reduce_day_of_month(
757 Calendar start, Calendar candidate) {
758
759 Calendar tempCal = (Calendar)candidate.clone();
760
761 tempCal.add(Calendar.MONTH, -1);
762
763 int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
764
765 if (delta > 0) {
766 delta -= tempCal.getActualMaximum(Calendar.DATE);
767 }
768
769 candidate.add(Calendar.DATE, delta);
770
771 while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
772 tempCal.add(Calendar.MONTH, -1);
773 candidate.add(
774 Calendar.DATE, -tempCal.getActualMaximum(Calendar.DATE));
775 }
776 }
777
778
781 protected static void reduce_day_of_year(
782 Calendar start, Calendar candidate) {
783
784 if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH)) ||
785 ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH)) &&
786 (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
787
788 candidate.add(Calendar.YEAR, -1);
789 }
790
791
792
793 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
794 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
795
796 while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH)) ||
797 (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
798
799 candidate.add(Calendar.YEAR, -1);
800 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
801 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
802 }
803 }
804
805
810 protected boolean candidateIsInRecurrence(
811 Calendar candidate, boolean debug) {
812
813 if ((until != null) &&
814 (candidate.getTime().getTime() > until.getTime().getTime())) {
815
816
817
818 if (debug) {
819 System.err.println("after until");
820 }
821
822 return false;
823 }
824
825 if ((getRecurrenceCount(candidate) % interval) != 0) {
826
827
828
829 if (debug) {
830 System.err.println("not an interval rep");
831 }
832
833 return false;
834 }
835 else if ((occurrence > 0) &&
836 (getRecurrenceCount(candidate) >= occurrence)) {
837
838 return false;
839 }
840
841 if (!matchesByDay(candidate) ||!matchesByMonthDay(candidate)
842 ||!matchesByYearDay(candidate) ||!matchesByWeekNo(candidate)
843 ||!matchesByMonth(candidate)) {
844
845
846
847 if (debug) {
848 System.err.println("doesn't match a by*");
849 }
850
851 return false;
852 }
853
854 if (debug) {
855 System.err.println("All checks succeeded");
856 }
857
858 return true;
859 }
860
861
866 protected int getMinimumInterval() {
867 if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null) ||
868 (byYearDay != null)) {
869
870 return DAILY;
871 }
872 else if ((frequency == WEEKLY) || (byWeekNo != null)) {
873 return WEEKLY;
874 }
875 else if ((frequency == MONTHLY) || (byMonth != null)) {
876 return MONTHLY;
877 }
878 else if (frequency == YEARLY) {
879 return YEARLY;
880 }
881 else if (frequency == NO_RECURRENCE) {
882 return NO_RECURRENCE;
883 }
884 else {
885
886
887
888 throw new IllegalStateException(
889 "Internal error: Unknown frequency value");
890 }
891 }
892
893
898 protected int getRecurrenceCount(Calendar candidate) {
899 switch (frequency) {
900
901 case NO_RECURRENCE :
902 return 0;
903
904 case DAILY :
905 return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
906
907 case WEEKLY :
908 Calendar tempCand = (Calendar)candidate.clone();
909
910 tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
911
912 return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
913
914 case MONTHLY :
915 return
916 (int)(getMonthNumber(candidate) - getMonthNumber(dtStart));
917
918 case YEARLY :
919 return
920 candidate.get(Calendar.YEAR) - dtStart.get(Calendar.YEAR);
921
922 default :
923 throw new IllegalStateException("bad frequency internally...");
924 }
925 }
926
927
932 protected boolean matchesByDay(Calendar candidate) {
933 if ((byDay == null) || (byDay.length == 0)) {
934
935
936
937 return true;
938 }
939
940 int i;
941
942 for (i = 0; i < byDay.length; i++) {
943 if (matchesIndividualByDay(candidate, byDay[i])) {
944 return true;
945 }
946 }
947
948 return false;
949 }
950
951
956 protected boolean matchesByField(
957 int[] array, int field, Calendar candidate, boolean allowNegative) {
958
959 if ((array == null) || (array.length == 0)) {
960
961
962
963 return true;
964 }
965
966 int i;
967
968 for (i = 0; i < array.length; i++) {
969 int val;
970
971 if (allowNegative && (array[i] < 0)) {
972
973
974
975 int max = candidate.getActualMaximum(field);
976
977 val = (max + 1) + array[i];
978 }
979 else {
980 val = array[i];
981 }
982
983 if (val == candidate.get(field)) {
984 return true;
985 }
986 }
987
988 return false;
989 }
990
991
996 protected boolean matchesByMonth(Calendar candidate) {
997 return matchesByField(byMonth, Calendar.MONTH, candidate, false);
998 }
999
1000
1005 protected boolean matchesByMonthDay(Calendar candidate) {
1006 return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
1007 }
1008
1009
1014 protected boolean matchesByWeekNo(Calendar candidate) {
1015 return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1016 }
1017
1018
1023 protected boolean matchesByYearDay(Calendar candidate) {
1024 return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1025 }
1026
1027
1032 protected boolean matchesIndividualByDay(
1033 Calendar candidate, DayAndPosition pos) {
1034
1035 if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
1036 return false;
1037 }
1038
1039 int position = pos.getDayPosition();
1040
1041 if (position == 0) {
1042 return true;
1043 }
1044
1045 int field = Calendar.DAY_OF_MONTH;
1046
1047 if (position > 0) {
1048 int candidatePosition = ((candidate.get(field) - 1) / 7) + 1;
1049
1050 return (position == candidatePosition);
1051 }
1052 else {
1053
1054
1055
1056 int negativeCandidatePosition =
1057 ((candidate.getActualMaximum(field) - candidate.get(field)) /
1058 7) + 1;
1059
1060 return (-position == negativeCandidatePosition);
1061 }
1062 }
1063
1064
1069 protected String stringizeIntArray(int[] a) {
1070 if (a == null) {
1071 return "null";
1072 }
1073
1074 StringBundler sb = new StringBundler(2 * a.length + 1);
1075
1076 sb.append("[");
1077
1078 for (int i = 0; i < a.length; i++) {
1079 if (i != 0) {
1080 sb.append(",");
1081 }
1082
1083 sb.append(a[i]);
1084 }
1085
1086 sb.append("]");
1087
1088 return sb.toString();
1089 }
1090
1091
1094 protected DayAndPosition[] byDay;
1095
1096
1099 protected int[] byMonth;
1100
1101
1104 protected int[] byMonthDay;
1105
1106
1109 protected int[] byWeekNo;
1110
1111
1114 protected int[] byYearDay;
1115
1116
1119 protected Calendar dtStart;
1120
1121
1124 protected Duration duration;
1125
1126
1129 protected int frequency;
1130
1131
1134 protected int interval;
1135
1136
1139 protected int occurrence = 0;
1140
1141
1144 protected Calendar until;
1145
1146 }