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
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 }
547
548
551 public void setDuration(Duration d) {
552 duration = (Duration)d.clone();
553 }
554
555
558 public void setFrequency(int freq) {
559 if ((frequency != DAILY) && (frequency != WEEKLY) &&
560 (frequency != MONTHLY) && (frequency != YEARLY) &&
561 (frequency != NO_RECURRENCE)) {
562
563 throw new IllegalArgumentException("Invalid frequency");
564 }
565
566 frequency = freq;
567 }
568
569
572 public void setInterval(int intr) {
573 interval = (intr > 0) ? intr : 1;
574 }
575
576
579 public void setOccurrence(int occur) {
580 occurrence = occur;
581 }
582
583
586 public void setUntil(Calendar u) {
587 if (u == null) {
588 until = null;
589
590 return;
591 }
592
593 until = (Calendar)u.clone();
594
595 until.clear(Calendar.ZONE_OFFSET);
596 until.clear(Calendar.DST_OFFSET);
597 until.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
598 }
599
600
603 public void setWeekStart(int weekstart) {
604 dtStart.setFirstDayOfWeek(weekstart);
605 }
606
607
612 @Override
613 public String toString() {
614 StringBundler sb = new StringBundler();
615
616 sb.append(getClass().getName());
617 sb.append("[dtStart=");
618 sb.append((dtStart != null) ? dtStart.toString() : "null");
619 sb.append(",duration=");
620 sb.append((duration != null) ? duration.toString() : "null");
621 sb.append(",frequency=");
622 sb.append(frequency);
623 sb.append(",interval=");
624 sb.append(interval);
625 sb.append(",until=");
626 sb.append((until != null) ? until.toString() : "null");
627 sb.append(",byDay=");
628
629 if (byDay == null) {
630 sb.append("null");
631 }
632 else {
633 sb.append("[");
634
635 for (int i = 0; i < byDay.length; i++) {
636 if (i != 0) {
637 sb.append(",");
638 }
639
640 if (byDay[i] != null) {
641 sb.append(byDay[i].toString());
642 }
643 else {
644 sb.append("null");
645 }
646 }
647
648 sb.append("]");
649 }
650
651 sb.append(",byMonthDay=");
652 sb.append(stringizeIntArray(byMonthDay));
653 sb.append(",byYearDay=");
654 sb.append(stringizeIntArray(byYearDay));
655 sb.append(",byWeekNo=");
656 sb.append(stringizeIntArray(byWeekNo));
657 sb.append(",byMonth=");
658 sb.append(stringizeIntArray(byMonth));
659 sb.append(']');
660
661 return sb.toString();
662 }
663
664
669 protected static long getDayNumber(Calendar cal) {
670 Calendar tempCal = (Calendar)cal.clone();
671
672
673
674 tempCal.set(Calendar.MILLISECOND, 0);
675 tempCal.set(Calendar.SECOND, 0);
676 tempCal.set(Calendar.MINUTE, 0);
677 tempCal.set(Calendar.HOUR_OF_DAY, 0);
678
679 return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
680 }
681
682
687 protected static long getMonthNumber(Calendar cal) {
688 return
689 ((cal.get(Calendar.YEAR) - 1970) * 12L) +
690 (cal.get(Calendar.MONTH) - Calendar.JANUARY);
691 }
692
693
698 protected static long getWeekNumber(Calendar cal) {
699 Calendar tempCal = (Calendar)cal.clone();
700
701
702
703 tempCal.set(Calendar.MILLISECOND, 0);
704 tempCal.set(Calendar.SECOND, 0);
705 tempCal.set(Calendar.MINUTE, 0);
706 tempCal.set(Calendar.HOUR_OF_DAY, 0);
707
708
709
710 int delta =
711 tempCal.getFirstDayOfWeek() - tempCal.get(Calendar.DAY_OF_WEEK);
712
713 if (delta > 0) {
714 delta -= 7;
715 }
716
717
718
719
720
721
722 long weekEpoch =
723 (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24L * 60 * 60 *
724 1000L;
725
726 return
727 (tempCal.getTime().getTime() - weekEpoch) /
728 (7 * 24 * 60 * 60 * 1000);
729 }
730
731
734 protected static void reduce_constant_length_field(
735 int field, Calendar start, Calendar candidate) {
736
737 if ((start.getMaximum(field) != start.getLeastMaximum(field)) ||
738 (start.getMinimum(field) != start.getGreatestMinimum(field))) {
739
740 throw new IllegalArgumentException("Not a constant length field");
741 }
742
743 int fieldLength =
744 (start.getMaximum(field) - start.getMinimum(field) + 1);
745 int delta = start.get(field) - candidate.get(field);
746
747 if (delta > 0) {
748 delta -= fieldLength;
749 }
750
751 candidate.add(field, delta);
752 }
753
754
757 protected static void reduce_day_of_month(
758 Calendar start, Calendar candidate) {
759
760 Calendar tempCal = (Calendar)candidate.clone();
761
762 tempCal.add(Calendar.MONTH, -1);
763
764 int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
765
766 if (delta > 0) {
767 delta -= tempCal.getActualMaximum(Calendar.DATE);
768 }
769
770 candidate.add(Calendar.DATE, delta);
771
772 while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
773 tempCal.add(Calendar.MONTH, -1);
774 candidate.add(
775 Calendar.DATE, -tempCal.getActualMaximum(Calendar.DATE));
776 }
777 }
778
779
782 protected static void reduce_day_of_year(
783 Calendar start, Calendar candidate) {
784
785 if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH)) ||
786 ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH)) &&
787 (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
788
789 candidate.add(Calendar.YEAR, -1);
790 }
791
792
793
794 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
795 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
796
797 while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH)) ||
798 (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
799
800 candidate.add(Calendar.YEAR, -1);
801 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
802 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
803 }
804 }
805
806
811 protected boolean candidateIsInRecurrence(
812 Calendar candidate, boolean debug) {
813
814 if ((until != null) &&
815 (candidate.getTime().getTime() > until.getTime().getTime())) {
816
817
818
819 if (debug) {
820 System.err.println("after until");
821 }
822
823 return false;
824 }
825
826 if ((getRecurrenceCount(candidate) % interval) != 0) {
827
828
829
830 if (debug) {
831 System.err.println("not an interval rep");
832 }
833
834 return false;
835 }
836 else if ((occurrence > 0) &&
837 (getRecurrenceCount(candidate) >= occurrence)) {
838
839 return false;
840 }
841
842 if (!matchesByDay(candidate) || !matchesByMonthDay(candidate) ||
843 !matchesByYearDay(candidate) || !matchesByWeekNo(candidate) ||
844 !matchesByMonth(candidate)) {
845
846
847
848 if (debug) {
849 System.err.println("doesn't match a by*");
850 }
851
852 return false;
853 }
854
855 if (debug) {
856 System.err.println("All checks succeeded");
857 }
858
859 return true;
860 }
861
862
867 protected int getMinimumInterval() {
868 if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null) ||
869 (byYearDay != null)) {
870
871 return DAILY;
872 }
873 else if ((frequency == WEEKLY) || (byWeekNo != null)) {
874 return WEEKLY;
875 }
876 else if ((frequency == MONTHLY) || (byMonth != null)) {
877 return MONTHLY;
878 }
879 else if (frequency == YEARLY) {
880 return YEARLY;
881 }
882 else if (frequency == NO_RECURRENCE) {
883 return NO_RECURRENCE;
884 }
885 else {
886
887
888
889 throw new IllegalStateException(
890 "Internal error: Unknown frequency value");
891 }
892 }
893
894
899 protected int getRecurrenceCount(Calendar candidate) {
900 switch (frequency) {
901
902 case NO_RECURRENCE :
903 return 0;
904
905 case DAILY :
906 return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
907
908 case WEEKLY :
909 Calendar tempCand = (Calendar)candidate.clone();
910
911 tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
912
913 return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
914
915 case MONTHLY :
916 return
917 (int)(getMonthNumber(candidate) - getMonthNumber(dtStart));
918
919 case YEARLY :
920 return
921 candidate.get(Calendar.YEAR) - dtStart.get(Calendar.YEAR);
922
923 default :
924 throw new IllegalStateException("bad frequency internally...");
925 }
926 }
927
928
933 protected boolean matchesByDay(Calendar candidate) {
934 if ((byDay == null) || (byDay.length == 0)) {
935
936
937
938 return true;
939 }
940
941 int i;
942
943 for (i = 0; i < byDay.length; i++) {
944 if (matchesIndividualByDay(candidate, byDay[i])) {
945 return true;
946 }
947 }
948
949 return false;
950 }
951
952
957 protected boolean matchesByField(
958 int[] array, int field, Calendar candidate, boolean allowNegative) {
959
960 if ((array == null) || (array.length == 0)) {
961
962
963
964 return true;
965 }
966
967 int i;
968
969 for (i = 0; i < array.length; i++) {
970 int val;
971
972 if (allowNegative && (array[i] < 0)) {
973
974
975
976 int max = candidate.getActualMaximum(field);
977
978 val = (max + 1) + array[i];
979 }
980 else {
981 val = array[i];
982 }
983
984 if (val == candidate.get(field)) {
985 return true;
986 }
987 }
988
989 return false;
990 }
991
992
997 protected boolean matchesByMonth(Calendar candidate) {
998 return matchesByField(byMonth, Calendar.MONTH, candidate, false);
999 }
1000
1001
1006 protected boolean matchesByMonthDay(Calendar candidate) {
1007 return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
1008 }
1009
1010
1015 protected boolean matchesByWeekNo(Calendar candidate) {
1016 return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1017 }
1018
1019
1024 protected boolean matchesByYearDay(Calendar candidate) {
1025 return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1026 }
1027
1028
1033 protected boolean matchesIndividualByDay(
1034 Calendar candidate, DayAndPosition pos) {
1035
1036 if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
1037 return false;
1038 }
1039
1040 int position = pos.getDayPosition();
1041
1042 if (position == 0) {
1043 return true;
1044 }
1045
1046 int field = Calendar.DAY_OF_MONTH;
1047
1048 if (position > 0) {
1049 int candidatePosition = ((candidate.get(field) - 1) / 7) + 1;
1050
1051 return (position == candidatePosition);
1052 }
1053 else {
1054
1055
1056
1057 int negativeCandidatePosition =
1058 ((candidate.getActualMaximum(field) - candidate.get(field)) /
1059 7) + 1;
1060
1061 return (-position == negativeCandidatePosition);
1062 }
1063 }
1064
1065
1070 protected String stringizeIntArray(int[] a) {
1071 if (a == null) {
1072 return "null";
1073 }
1074
1075 StringBundler sb = new StringBundler(2 * a.length + 1);
1076
1077 sb.append("[");
1078
1079 for (int i = 0; i < a.length; i++) {
1080 if (i != 0) {
1081 sb.append(",");
1082 }
1083
1084 sb.append(a[i]);
1085 }
1086
1087 sb.append("]");
1088
1089 return sb.toString();
1090 }
1091
1092
1095 protected DayAndPosition[] byDay;
1096
1097
1100 protected int[] byMonth;
1101
1102
1105 protected int[] byMonthDay;
1106
1107
1110 protected int[] byWeekNo;
1111
1112
1115 protected int[] byYearDay;
1116
1117
1120 protected Calendar dtStart;
1121
1122
1125 protected Duration duration;
1126
1127
1130 protected int frequency;
1131
1132
1135 protected int interval;
1136
1137
1140 protected int occurrence = 0;
1141
1142
1145 protected Calendar until;
1146
1147 }