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 WEEKLY = 4;
072
073
076 public static final int MONTHLY = 5;
077
078
081 public static final int YEARLY = 6;
082
083
086 public static final int NO_RECURRENCE = 7;
087
088
091 protected Calendar dtStart;
092
093
096 protected Duration duration;
097
098
101 protected int frequency;
102
103
106 protected int interval;
107
108
111 protected int occurrence = 0;
112
113
116 protected Calendar until;
117
118
121 protected DayAndPosition[] byDay;
122
123
126 protected int[] byMonthDay;
127
128
131 protected int[] byYearDay;
132
133
136 protected int[] byWeekNo;
137
138
141 protected int[] byMonth;
142
143
146 public Recurrence() {
147 this(null, new Duration(), NO_RECURRENCE);
148 }
149
150
153 public Recurrence(Calendar start, Duration dur) {
154 this(start, dur, NO_RECURRENCE);
155 }
156
157
160 public Recurrence(Calendar start, Duration dur, int freq) {
161 setDtStart(start);
162
163 duration = (Duration)dur.clone();
164 frequency = freq;
165 interval = 1;
166 }
167
168
169
170
175 public Calendar getDtStart() {
176 return (Calendar)dtStart.clone();
177 }
178
179
182 public void setDtStart(Calendar start) {
183 int oldStart;
184
185 if (dtStart != null) {
186 oldStart = dtStart.getFirstDayOfWeek();
187 }
188 else {
189 oldStart = Calendar.MONDAY;
190 }
191
192 if (start == null) {
193 dtStart = CalendarFactoryUtil.getCalendar(
194 TimeZoneUtil.getTimeZone(StringPool.UTC));
195
196 dtStart.setTime(new Date(0L));
197 }
198 else {
199 dtStart = (Calendar)start.clone();
200
201 dtStart.clear(Calendar.ZONE_OFFSET);
202 dtStart.clear(Calendar.DST_OFFSET);
203 dtStart.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
204 }
205
206 dtStart.setMinimalDaysInFirstWeek(4);
207 dtStart.setFirstDayOfWeek(oldStart);
208 }
209
210
215 public Duration getDuration() {
216 return (Duration)duration.clone();
217 }
218
219
222 public void setDuration(Duration d) {
223 duration = (Duration)d.clone();
224 }
225
226
231 public Calendar getDtEnd() {
232
233
237 Calendar tempEnd = (Calendar)dtStart.clone();
238
239 tempEnd.setTime(new Date(dtStart.getTime().getTime()
240 + duration.getInterval()));
241
242 return tempEnd;
243 }
244
245
248 public void setDtEnd(Calendar end) {
249 Calendar tempEnd = (Calendar)end.clone();
250
251 tempEnd.clear(Calendar.ZONE_OFFSET);
252 tempEnd.clear(Calendar.DST_OFFSET);
253 tempEnd.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
254 duration.setInterval(tempEnd.getTime().getTime()
255 - dtStart.getTime().getTime());
256 }
257
258
263 public int getFrequency() {
264 return frequency;
265 }
266
267
270 public void setFrequency(int freq) {
271 if ((frequency != DAILY) && (frequency != WEEKLY)
272 && (frequency != MONTHLY) && (frequency != YEARLY)
273 && (frequency != NO_RECURRENCE)) {
274 throw new IllegalArgumentException("Invalid frequency");
275 }
276
277 frequency = freq;
278 }
279
280
285 public int getInterval() {
286 return interval;
287 }
288
289
292 public void setInterval(int intr) {
293 interval = (intr > 0) ? intr : 1;
294 }
295
296
301 public int getOccurrence() {
302 return occurrence;
303 }
304
305
308 public void setOccurrence(int occur) {
309 occurrence = occur;
310 }
311
312
317 public Calendar getUntil() {
318 return ((until != null) ? (Calendar)until.clone() : null);
319 }
320
321
324 public void setUntil(Calendar u) {
325 if (u == null) {
326 until = null;
327
328 return;
329 }
330
331 until = (Calendar)u.clone();
332
333 until.clear(Calendar.ZONE_OFFSET);
334 until.clear(Calendar.DST_OFFSET);
335 until.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
336 }
337
338
343 public int getWeekStart() {
344 return dtStart.getFirstDayOfWeek();
345 }
346
347
350 public void setWeekStart(int weekstart) {
351 dtStart.setFirstDayOfWeek(weekstart);
352 }
353
354
359 public DayAndPosition[] getByDay() {
360 if (byDay == null) {
361 return null;
362 }
363
364 DayAndPosition[] b = new DayAndPosition[byDay.length];
365
366
370 for (int i = 0; i < byDay.length; i++) {
371 b[i] = (DayAndPosition)byDay[i].clone();
372 }
373
374 return b;
375 }
376
377
380 public void setByDay(DayAndPosition[] b) {
381 if (b == null) {
382 byDay = null;
383
384 return;
385 }
386
387 byDay = new DayAndPosition[b.length];
388
389
393 for (int i = 0; i < b.length; i++) {
394 byDay[i] = (DayAndPosition)b[i].clone();
395 }
396 }
397
398
403 public int[] getByMonthDay() {
404 if (byMonthDay == null) {
405 return null;
406 }
407
408 int[] b = new int[byMonthDay.length];
409
410 System.arraycopy(byMonthDay, 0, b, 0, byMonthDay.length);
411
412 return b;
413 }
414
415
418 public void setByMonthDay(int[] b) {
419 if (b == null) {
420 byMonthDay = null;
421
422 return;
423 }
424
425 byMonthDay = new int[b.length];
426
427 System.arraycopy(b, 0, byMonthDay, 0, b.length);
428 }
429
430
435 public int[] getByYearDay() {
436 if (byYearDay == null) {
437 return null;
438 }
439
440 int[] b = new int[byYearDay.length];
441
442 System.arraycopy(byYearDay, 0, b, 0, byYearDay.length);
443
444 return b;
445 }
446
447
450 public void setByYearDay(int[] b) {
451 if (b == null) {
452 byYearDay = null;
453
454 return;
455 }
456
457 byYearDay = new int[b.length];
458
459 System.arraycopy(b, 0, byYearDay, 0, b.length);
460 }
461
462
467 public int[] getByWeekNo() {
468 if (byWeekNo == null) {
469 return null;
470 }
471
472 int[] b = new int[byWeekNo.length];
473
474 System.arraycopy(byWeekNo, 0, b, 0, byWeekNo.length);
475
476 return b;
477 }
478
479
482 public void setByWeekNo(int[] b) {
483 if (b == null) {
484 byWeekNo = null;
485
486 return;
487 }
488
489 byWeekNo = new int[b.length];
490
491 System.arraycopy(b, 0, byWeekNo, 0, b.length);
492 }
493
494
499 public int[] getByMonth() {
500 if (byMonth == null) {
501 return null;
502 }
503
504 int[] b = new int[byMonth.length];
505
506 System.arraycopy(byMonth, 0, b, 0, byMonth.length);
507
508 return b;
509 }
510
511
514 public void setByMonth(int[] b) {
515 if (b == null) {
516 byMonth = null;
517
518 return;
519 }
520
521 byMonth = new int[b.length];
522
523 System.arraycopy(b, 0, byMonth, 0, b.length);
524 }
525
526
531 public boolean isInRecurrence(Calendar current) {
532 return isInRecurrence(current, false);
533 }
534
535
540 public boolean isInRecurrence(Calendar current, boolean debug) {
541 Calendar myCurrent = (Calendar)current.clone();
542
543
544
545 myCurrent.clear(Calendar.ZONE_OFFSET);
546 myCurrent.clear(Calendar.DST_OFFSET);
547 myCurrent.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
548 myCurrent.setMinimalDaysInFirstWeek(4);
549 myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
550
551 if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
552
553
554
555 if (debug) {
556 System.err.println("current < start");
557 }
558
559 return false;
560 }
561
562 Calendar candidate = getCandidateStartTime(myCurrent);
563
564
565
566 while (candidate.getTime().getTime() + duration.getInterval()
567 > myCurrent.getTime().getTime()) {
568 if (candidateIsInRecurrence(candidate, debug)) {
569 return true;
570 }
571
572
573
574 candidate.add(Calendar.SECOND, -1);
575
576
577
578 if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
579 if (debug) {
580 System.err.println("No candidates after dtStart");
581 }
582
583 return false;
584 }
585
586 candidate = getCandidateStartTime(candidate);
587 }
588
589 if (debug) {
590 System.err.println("No matching candidates");
591 }
592
593 return false;
594 }
595
596
601 protected boolean candidateIsInRecurrence(Calendar candidate,
602 boolean debug) {
603 if ((until != null)
604 && (candidate.getTime().getTime() > until.getTime().getTime())) {
605
606
607
608 if (debug) {
609 System.err.println("after until");
610 }
611
612 return false;
613 }
614
615 if (getRecurrenceCount(candidate) % interval != 0) {
616
617
618
619 if (debug) {
620 System.err.println("not an interval rep");
621 }
622
623 return false;
624 }
625 else if ((occurrence > 0) &&
626 (getRecurrenceCount(candidate) >= occurrence)) {
627
628 return false;
629 }
630
631 if (!matchesByDay(candidate) ||!matchesByMonthDay(candidate)
632 ||!matchesByYearDay(candidate) ||!matchesByWeekNo(candidate)
633 ||!matchesByMonth(candidate)) {
634
635
636
637 if (debug) {
638 System.err.println("doesn't match a by*");
639 }
640
641 return false;
642 }
643
644 if (debug) {
645 System.err.println("All checks succeeded");
646 }
647
648 return true;
649 }
650
651
656 protected int getMinimumInterval() {
657 if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null)
658 || (byYearDay != null)) {
659 return DAILY;
660 }
661 else if ((frequency == WEEKLY) || (byWeekNo != null)) {
662 return WEEKLY;
663 }
664 else if ((frequency == MONTHLY) || (byMonth != null)) {
665 return MONTHLY;
666 }
667 else if (frequency == YEARLY) {
668 return YEARLY;
669 }
670 else if (frequency == NO_RECURRENCE) {
671 return NO_RECURRENCE;
672 }
673 else {
674
675
676
677 throw new IllegalStateException(
678 "Internal error: Unknown frequency value");
679 }
680 }
681
682
687 public Calendar getCandidateStartTime(Calendar current) {
688 if (dtStart.getTime().getTime() > current.getTime().getTime()) {
689 throw new IllegalArgumentException("Current time before DtStart");
690 }
691
692 int minInterval = getMinimumInterval();
693 Calendar candidate = (Calendar)current.clone();
694
695 if (true) {
696
697
698
699 candidate.clear(Calendar.ZONE_OFFSET);
700 candidate.clear(Calendar.DST_OFFSET);
701 candidate.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
702 candidate.setMinimalDaysInFirstWeek(4);
703 candidate.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
704 }
705
706 if (frequency == NO_RECURRENCE) {
707 candidate.setTime(dtStart.getTime());
708
709 return candidate;
710 }
711
712 reduce_constant_length_field(Calendar.SECOND, dtStart, candidate);
713 reduce_constant_length_field(Calendar.MINUTE, dtStart, candidate);
714 reduce_constant_length_field(Calendar.HOUR_OF_DAY, dtStart, candidate);
715
716 switch (minInterval) {
717
718 case DAILY :
719
720
721
722 break;
723
724 case WEEKLY :
725 reduce_constant_length_field(Calendar.DAY_OF_WEEK, dtStart,
726 candidate);
727 break;
728
729 case MONTHLY :
730 reduce_day_of_month(dtStart, candidate);
731 break;
732
733 case YEARLY :
734 reduce_day_of_year(dtStart, candidate);
735 break;
736 }
737
738 return candidate;
739 }
740
741
744 protected static void reduce_constant_length_field(int field,
745 Calendar start,
746 Calendar candidate) {
747 if ((start.getMaximum(field) != start.getLeastMaximum(field))
748 || (start.getMinimum(field) != start.getGreatestMinimum(field))) {
749 throw new IllegalArgumentException("Not a constant length field");
750 }
751
752 int fieldLength = (start.getMaximum(field) - start.getMinimum(field)
753 + 1);
754 int delta = start.get(field) - candidate.get(field);
755
756 if (delta > 0) {
757 delta -= fieldLength;
758 }
759
760 candidate.add(field, delta);
761 }
762
763
766 protected static void reduce_day_of_month(Calendar start,
767 Calendar candidate) {
768 Calendar tempCal = (Calendar)candidate.clone();
769
770 tempCal.add(Calendar.MONTH, -1);
771
772 int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
773
774 if (delta > 0) {
775 delta -= tempCal.getActualMaximum(Calendar.DATE);
776 }
777
778 candidate.add(Calendar.DATE, delta);
779
780 while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
781 tempCal.add(Calendar.MONTH, -1);
782 candidate.add(Calendar.DATE,
783 -tempCal.getActualMaximum(Calendar.DATE));
784 }
785 }
786
787
790 protected static void reduce_day_of_year(Calendar start,
791 Calendar candidate) {
792 if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH))
793 || ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH))
794 && (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
795 candidate.add(Calendar.YEAR, -1);
796 }
797
798
799
800 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
801 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
802
803 while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH))
804 || (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
805 candidate.add(Calendar.YEAR, -1);
806 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
807 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
808 }
809 }
810
811
816 protected int getRecurrenceCount(Calendar candidate) {
817 switch (frequency) {
818
819 case NO_RECURRENCE :
820 return 0;
821
822 case DAILY :
823 return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
824
825 case WEEKLY :
826 Calendar tempCand = (Calendar)candidate.clone();
827
828 tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
829
830 return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
831
832 case MONTHLY :
833 return (int)(getMonthNumber(candidate)
834 - getMonthNumber(dtStart));
835
836 case YEARLY :
837 return candidate.get(Calendar.YEAR)
838 - dtStart.get(Calendar.YEAR);
839
840 default :
841 throw new IllegalStateException("bad frequency internally...");
842 }
843 }
844
845
850 protected static long getDayNumber(Calendar cal) {
851 Calendar tempCal = (Calendar)cal.clone();
852
853
854
855 tempCal.set(Calendar.MILLISECOND, 0);
856 tempCal.set(Calendar.SECOND, 0);
857 tempCal.set(Calendar.MINUTE, 0);
858 tempCal.set(Calendar.HOUR_OF_DAY, 0);
859
860 return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
861 }
862
863
868 protected static long getWeekNumber(Calendar cal) {
869 Calendar tempCal = (Calendar)cal.clone();
870
871
872
873 tempCal.set(Calendar.MILLISECOND, 0);
874 tempCal.set(Calendar.SECOND, 0);
875 tempCal.set(Calendar.MINUTE, 0);
876 tempCal.set(Calendar.HOUR_OF_DAY, 0);
877
878
879
880 int delta = tempCal.getFirstDayOfWeek()
881 - tempCal.get(Calendar.DAY_OF_WEEK);
882
883 if (delta > 0) {
884 delta -= 7;
885 }
886
887
888
889
890
891
892 long weekEpoch = (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24L
893 * 60 * 60 * 1000L;
894
895 return (tempCal.getTime().getTime() - weekEpoch)
896 / (7 * 24 * 60 * 60 * 1000);
897 }
898
899
904 protected static long getMonthNumber(Calendar cal) {
905 return (cal.get(Calendar.YEAR) - 1970) * 12L
906 + (cal.get(Calendar.MONTH) - Calendar.JANUARY);
907 }
908
909
914 protected boolean matchesByDay(Calendar candidate) {
915 if ((byDay == null) || (byDay.length == 0)) {
916
917
918
919 return true;
920 }
921
922 int i;
923
924 for (i = 0; i < byDay.length; i++) {
925 if (matchesIndividualByDay(candidate, byDay[i])) {
926 return true;
927 }
928 }
929
930 return false;
931 }
932
933
938 protected boolean matchesIndividualByDay(Calendar candidate,
939 DayAndPosition pos) {
940 if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
941 return false;
942 }
943
944 int position = pos.getDayPosition();
945
946 if (position == 0) {
947 return true;
948 }
949
950 int field = Calendar.DAY_OF_MONTH;
951
952 if (position > 0) {
953 int candidatePosition = ((candidate.get(field) - 1) / 7) + 1;
954
955 return (position == candidatePosition);
956 }
957 else {
958
959
960
961 int negativeCandidatePosition =
962 ((candidate.getActualMaximum(field) - candidate.get(field)) / 7)
963 + 1;
964
965 return (-position == negativeCandidatePosition);
966 }
967 }
968
969
974 protected static boolean matchesByField(int[] array, int field,
975 Calendar candidate,
976 boolean allowNegative) {
977 if ((array == null) || (array.length == 0)) {
978
979
980
981 return true;
982 }
983
984 int i;
985
986 for (i = 0; i < array.length; i++) {
987 int val;
988
989 if (allowNegative && (array[i] < 0)) {
990
991
992
993 int max = candidate.getActualMaximum(field);
994
995 val = (max + 1) + array[i];
996 }
997 else {
998 val = array[i];
999 }
1000
1001 if (val == candidate.get(field)) {
1002 return true;
1003 }
1004 }
1005
1006 return false;
1007 }
1008
1009
1014 protected boolean matchesByMonthDay(Calendar candidate) {
1015 return matchesByField(byMonthDay, Calendar.DATE, 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 matchesByWeekNo(Calendar candidate) {
1033 return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1034 }
1035
1036
1041 protected boolean matchesByMonth(Calendar candidate) {
1042 return matchesByField(byMonth, Calendar.MONTH, candidate, false);
1043 }
1044
1045
1050 @Override
1051 public String toString() {
1052 StringBundler sb = new StringBundler();
1053
1054 sb.append(getClass().getName());
1055 sb.append("[dtStart=");
1056 sb.append((dtStart != null) ? dtStart.toString() : "null");
1057 sb.append(",duration=");
1058 sb.append((duration != null) ? duration.toString() : "null");
1059 sb.append(",frequency=");
1060 sb.append(frequency);
1061 sb.append(",interval=");
1062 sb.append(interval);
1063 sb.append(",until=");
1064 sb.append((until != null) ? until.toString() : "null");
1065 sb.append(",byDay=");
1066
1067 if (byDay == null) {
1068 sb.append("null");
1069 }
1070 else {
1071 sb.append("[");
1072
1073 for (int i = 0; i < byDay.length; i++) {
1074 if (i != 0) {
1075 sb.append(",");
1076 }
1077
1078 if (byDay[i] != null) {
1079 sb.append(byDay[i].toString());
1080 }
1081 else {
1082 sb.append("null");
1083 }
1084 }
1085
1086 sb.append("]");
1087 }
1088
1089 sb.append(",byMonthDay=");
1090 sb.append(stringizeIntArray(byMonthDay));
1091 sb.append(",byYearDay=");
1092 sb.append(stringizeIntArray(byYearDay));
1093 sb.append(",byWeekNo=");
1094 sb.append(stringizeIntArray(byWeekNo));
1095 sb.append(",byMonth=");
1096 sb.append(stringizeIntArray(byMonth));
1097 sb.append(']');
1098
1099 return sb.toString();
1100 }
1101
1102
1107 private String stringizeIntArray(int[] a) {
1108 if (a == null) {
1109 return "null";
1110 }
1111
1112 StringBundler sb = new StringBundler(2 * a.length + 1);
1113
1114 sb.append("[");
1115
1116 for (int i = 0; i < a.length; i++) {
1117 if (i != 0) {
1118 sb.append(",");
1119 }
1120
1121 sb.append(a[i]);
1122 }
1123
1124 sb.append("]");
1125
1126 return sb.toString();
1127 }
1128
1129 }