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 myCurrent.set(Calendar.SECOND, 0);
372 myCurrent.set(Calendar.MILLISECOND, 0);
373
374 if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
375
376
377
378 if (debug) {
379 System.err.println("current < start");
380 }
381
382 return false;
383 }
384
385 Calendar candidate = getCandidateStartTime(myCurrent);
386
387
388
389 while ((candidate.getTime().getTime() + duration.getInterval()) >
390 myCurrent.getTime().getTime()) {
391
392 if (candidateIsInRecurrence(candidate, debug)) {
393 return true;
394 }
395
396
397
398 candidate.add(Calendar.SECOND, -1);
399
400
401
402 if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
403 if (debug) {
404 System.err.println("No candidates after dtStart");
405 }
406
407 return false;
408 }
409
410 candidate = getCandidateStartTime(candidate);
411 }
412
413 if (debug) {
414 System.err.println("No matching candidates");
415 }
416
417 return false;
418 }
419
420
423 public void setByDay(DayAndPosition[] b) {
424 if (b == null) {
425 byDay = null;
426
427 return;
428 }
429
430 byDay = new DayAndPosition[b.length];
431
432
436 for (int i = 0; i < b.length; i++) {
437 byDay[i] = (DayAndPosition)b[i].clone();
438 }
439 }
440
441
444 public void setByMonth(int[] b) {
445 if (b == null) {
446 byMonth = null;
447
448 return;
449 }
450
451 byMonth = new int[b.length];
452
453 System.arraycopy(b, 0, byMonth, 0, b.length);
454 }
455
456
459 public void setByMonthDay(int[] b) {
460 if (b == null) {
461 byMonthDay = null;
462
463 return;
464 }
465
466 byMonthDay = new int[b.length];
467
468 System.arraycopy(b, 0, byMonthDay, 0, b.length);
469 }
470
471
474 public void setByWeekNo(int[] b) {
475 if (b == null) {
476 byWeekNo = null;
477
478 return;
479 }
480
481 byWeekNo = new int[b.length];
482
483 System.arraycopy(b, 0, byWeekNo, 0, b.length);
484 }
485
486
489 public void setByYearDay(int[] b) {
490 if (b == null) {
491 byYearDay = null;
492
493 return;
494 }
495
496 byYearDay = new int[b.length];
497
498 System.arraycopy(b, 0, byYearDay, 0, b.length);
499 }
500
501
504 public void setDtEnd(Calendar end) {
505 Calendar tempEnd = (Calendar)end.clone();
506
507 tempEnd.clear(Calendar.ZONE_OFFSET);
508 tempEnd.clear(Calendar.DST_OFFSET);
509 tempEnd.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
510 duration.setInterval(
511 tempEnd.getTime().getTime() - dtStart.getTime().getTime());
512 }
513
514
517 public void setDtStart(Calendar start) {
518 int oldStart;
519
520 if (dtStart != null) {
521 oldStart = dtStart.getFirstDayOfWeek();
522 }
523 else {
524 oldStart = Calendar.MONDAY;
525 }
526
527 if (start == null) {
528 dtStart = CalendarFactoryUtil.getCalendar(
529 TimeZoneUtil.getTimeZone(StringPool.UTC));
530
531 dtStart.setTime(new Date(0L));
532 }
533 else {
534 dtStart = (Calendar)start.clone();
535
536 dtStart.clear(Calendar.ZONE_OFFSET);
537 dtStart.clear(Calendar.DST_OFFSET);
538 dtStart.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
539 }
540
541 dtStart.setMinimalDaysInFirstWeek(4);
542 dtStart.setFirstDayOfWeek(oldStart);
543 dtStart.set(Calendar.SECOND, 0);
544 dtStart.set(Calendar.MILLISECOND, 0);
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 1000;
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 }