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 WEEKLY = 4;
073
074
077 public static final int MONTHLY = 5;
078
079
082 public static final int YEARLY = 6;
083
084
087 public static final int NO_RECURRENCE = 7;
088
089
092 protected Calendar dtStart;
093
094
097 protected Duration duration;
098
099
102 protected int frequency;
103
104
107 protected int interval;
108
109
112 protected int occurrence = 0;
113
114
117 protected Calendar until;
118
119
122 protected DayAndPosition[] byDay;
123
124
127 protected int[] byMonthDay;
128
129
132 protected int[] byYearDay;
133
134
137 protected int[] byWeekNo;
138
139
142 protected int[] byMonth;
143
144
147 public Recurrence() {
148 this(null, new Duration(), NO_RECURRENCE);
149 }
150
151
154 public Recurrence(Calendar start, Duration dur) {
155 this(start, dur, NO_RECURRENCE);
156 }
157
158
161 public Recurrence(Calendar start, Duration dur, int freq) {
162 setDtStart(start);
163
164 duration = (Duration)dur.clone();
165 frequency = freq;
166 interval = 1;
167 }
168
169
170
171
176 public Calendar getDtStart() {
177 return (Calendar)dtStart.clone();
178 }
179
180
183 public void setDtStart(Calendar start) {
184 int oldStart;
185
186 if (dtStart != null) {
187 oldStart = dtStart.getFirstDayOfWeek();
188 }
189 else {
190 oldStart = Calendar.MONDAY;
191 }
192
193 if (start == null) {
194 dtStart = CalendarFactoryUtil.getCalendar(
195 TimeZoneUtil.getTimeZone(StringPool.UTC));
196
197 dtStart.setTime(new Date(0L));
198 }
199 else {
200 dtStart = (Calendar)start.clone();
201
202 dtStart.clear(Calendar.ZONE_OFFSET);
203 dtStart.clear(Calendar.DST_OFFSET);
204 dtStart.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
205 }
206
207 dtStart.setMinimalDaysInFirstWeek(4);
208 dtStart.setFirstDayOfWeek(oldStart);
209 }
210
211
216 public Duration getDuration() {
217 return (Duration)duration.clone();
218 }
219
220
223 public void setDuration(Duration d) {
224 duration = (Duration)d.clone();
225 }
226
227
232 public Calendar getDtEnd() {
233
234
238 Calendar tempEnd = (Calendar)dtStart.clone();
239
240 tempEnd.setTime(new Date(dtStart.getTime().getTime()
241 + duration.getInterval()));
242
243 return tempEnd;
244 }
245
246
249 public void setDtEnd(Calendar end) {
250 Calendar tempEnd = (Calendar)end.clone();
251
252 tempEnd.clear(Calendar.ZONE_OFFSET);
253 tempEnd.clear(Calendar.DST_OFFSET);
254 tempEnd.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
255 duration.setInterval(tempEnd.getTime().getTime()
256 - dtStart.getTime().getTime());
257 }
258
259
264 public int getFrequency() {
265 return frequency;
266 }
267
268
271 public void setFrequency(int freq) {
272 if ((frequency != DAILY) && (frequency != WEEKLY)
273 && (frequency != MONTHLY) && (frequency != YEARLY)
274 && (frequency != NO_RECURRENCE)) {
275 throw new IllegalArgumentException("Invalid frequency");
276 }
277
278 frequency = freq;
279 }
280
281
286 public int getInterval() {
287 return interval;
288 }
289
290
293 public void setInterval(int intr) {
294 interval = (intr > 0) ? intr : 1;
295 }
296
297
302 public int getOccurrence() {
303 return occurrence;
304 }
305
306
309 public void setOccurrence(int occur) {
310 occurrence = occur;
311 }
312
313
318 public Calendar getUntil() {
319 return ((until != null) ? (Calendar)until.clone() : null);
320 }
321
322
325 public void setUntil(Calendar u) {
326 if (u == null) {
327 until = null;
328
329 return;
330 }
331
332 until = (Calendar)u.clone();
333
334 until.clear(Calendar.ZONE_OFFSET);
335 until.clear(Calendar.DST_OFFSET);
336 until.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
337 }
338
339
344 public int getWeekStart() {
345 return dtStart.getFirstDayOfWeek();
346 }
347
348
351 public void setWeekStart(int weekstart) {
352 dtStart.setFirstDayOfWeek(weekstart);
353 }
354
355
360 public DayAndPosition[] getByDay() {
361 if (byDay == null) {
362 return null;
363 }
364
365 DayAndPosition[] b = new DayAndPosition[byDay.length];
366
367
371 for (int i = 0; i < byDay.length; i++) {
372 b[i] = (DayAndPosition)byDay[i].clone();
373 }
374
375 return b;
376 }
377
378
381 public void setByDay(DayAndPosition[] b) {
382 if (b == null) {
383 byDay = null;
384
385 return;
386 }
387
388 byDay = new DayAndPosition[b.length];
389
390
394 for (int i = 0; i < b.length; i++) {
395 byDay[i] = (DayAndPosition)b[i].clone();
396 }
397 }
398
399
404 public int[] getByMonthDay() {
405 if (byMonthDay == null) {
406 return null;
407 }
408
409 int[] b = new int[byMonthDay.length];
410
411 System.arraycopy(byMonthDay, 0, b, 0, byMonthDay.length);
412
413 return b;
414 }
415
416
419 public void setByMonthDay(int[] b) {
420 if (b == null) {
421 byMonthDay = null;
422
423 return;
424 }
425
426 byMonthDay = new int[b.length];
427
428 System.arraycopy(b, 0, byMonthDay, 0, b.length);
429 }
430
431
436 public int[] getByYearDay() {
437 if (byYearDay == null) {
438 return null;
439 }
440
441 int[] b = new int[byYearDay.length];
442
443 System.arraycopy(byYearDay, 0, b, 0, byYearDay.length);
444
445 return b;
446 }
447
448
451 public void setByYearDay(int[] b) {
452 if (b == null) {
453 byYearDay = null;
454
455 return;
456 }
457
458 byYearDay = new int[b.length];
459
460 System.arraycopy(b, 0, byYearDay, 0, b.length);
461 }
462
463
468 public int[] getByWeekNo() {
469 if (byWeekNo == null) {
470 return null;
471 }
472
473 int[] b = new int[byWeekNo.length];
474
475 System.arraycopy(byWeekNo, 0, b, 0, byWeekNo.length);
476
477 return b;
478 }
479
480
483 public void setByWeekNo(int[] b) {
484 if (b == null) {
485 byWeekNo = null;
486
487 return;
488 }
489
490 byWeekNo = new int[b.length];
491
492 System.arraycopy(b, 0, byWeekNo, 0, b.length);
493 }
494
495
500 public int[] getByMonth() {
501 if (byMonth == null) {
502 return null;
503 }
504
505 int[] b = new int[byMonth.length];
506
507 System.arraycopy(byMonth, 0, b, 0, byMonth.length);
508
509 return b;
510 }
511
512
515 public void setByMonth(int[] b) {
516 if (b == null) {
517 byMonth = null;
518
519 return;
520 }
521
522 byMonth = new int[b.length];
523
524 System.arraycopy(b, 0, byMonth, 0, b.length);
525 }
526
527
532 public boolean isInRecurrence(Calendar current) {
533 return isInRecurrence(current, false);
534 }
535
536
541 public boolean isInRecurrence(Calendar current, boolean debug) {
542 Calendar myCurrent = (Calendar)current.clone();
543
544
545
546 myCurrent.clear(Calendar.ZONE_OFFSET);
547 myCurrent.clear(Calendar.DST_OFFSET);
548 myCurrent.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
549 myCurrent.setMinimalDaysInFirstWeek(4);
550 myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
551
552 if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
553
554
555
556 if (debug) {
557 System.err.println("current < start");
558 }
559
560 return false;
561 }
562
563 if (myCurrent.getTime().getTime()
564 < dtStart.getTime().getTime() + duration.getInterval()) {
565
566
567
568 if (debug) {
569 System.err.println("within duration of start");
570 }
571
572 return true;
573 }
574
575 Calendar candidate = getCandidateStartTime(myCurrent);
576
577
578
579 while (candidate.getTime().getTime() + duration.getInterval()
580 > myCurrent.getTime().getTime()) {
581 if (candidateIsInRecurrence(candidate, debug)) {
582 return true;
583 }
584
585
586
587 candidate.add(Calendar.SECOND, -1);
588
589
590
591 if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
592 if (debug) {
593 System.err.println("No candidates after dtStart");
594 }
595
596 return false;
597 }
598
599 candidate = getCandidateStartTime(candidate);
600 }
601
602 if (debug) {
603 System.err.println("No matching candidates");
604 }
605
606 return false;
607 }
608
609
614 protected boolean candidateIsInRecurrence(Calendar candidate,
615 boolean debug) {
616 if ((until != null)
617 && (candidate.getTime().getTime() > until.getTime().getTime())) {
618
619
620
621 if (debug) {
622 System.err.println("after until");
623 }
624
625 return false;
626 }
627
628 if (getRecurrenceCount(candidate) % interval != 0) {
629
630
631
632 if (debug) {
633 System.err.println("not an interval rep");
634 }
635
636 return false;
637 }
638 else if ((occurrence > 0) &&
639 (getRecurrenceCount(candidate) >= occurrence)) {
640
641 return false;
642 }
643
644 if (!matchesByDay(candidate) ||!matchesByMonthDay(candidate)
645 ||!matchesByYearDay(candidate) ||!matchesByWeekNo(candidate)
646 ||!matchesByMonth(candidate)) {
647
648
649
650 if (debug) {
651 System.err.println("doesn't match a by*");
652 }
653
654 return false;
655 }
656
657 if (debug) {
658 System.err.println("All checks succeeded");
659 }
660
661 return true;
662 }
663
664
669 protected int getMinimumInterval() {
670 if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null)
671 || (byYearDay != null)) {
672 return DAILY;
673 }
674 else if ((frequency == WEEKLY) || (byWeekNo != null)) {
675 return WEEKLY;
676 }
677 else if ((frequency == MONTHLY) || (byMonth != null)) {
678 return MONTHLY;
679 }
680 else if (frequency == YEARLY) {
681 return YEARLY;
682 }
683 else if (frequency == NO_RECURRENCE) {
684 return NO_RECURRENCE;
685 }
686 else {
687
688
689
690 throw new IllegalStateException(
691 "Internal error: Unknown frequency value");
692 }
693 }
694
695
700 public Calendar getCandidateStartTime(Calendar current) {
701 if (dtStart.getTime().getTime() > current.getTime().getTime()) {
702 throw new IllegalArgumentException("Current time before DtStart");
703 }
704
705 int minInterval = getMinimumInterval();
706 Calendar candidate = (Calendar)current.clone();
707
708 if (true) {
709
710
711
712 candidate.clear(Calendar.ZONE_OFFSET);
713 candidate.clear(Calendar.DST_OFFSET);
714 candidate.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
715 candidate.setMinimalDaysInFirstWeek(4);
716 candidate.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
717 }
718
719 if (frequency == NO_RECURRENCE) {
720 candidate.setTime(dtStart.getTime());
721
722 return candidate;
723 }
724
725 reduce_constant_length_field(Calendar.SECOND, dtStart, candidate);
726 reduce_constant_length_field(Calendar.MINUTE, dtStart, candidate);
727 reduce_constant_length_field(Calendar.HOUR_OF_DAY, dtStart, candidate);
728
729 switch (minInterval) {
730
731 case DAILY :
732
733
734
735 break;
736
737 case WEEKLY :
738 reduce_constant_length_field(Calendar.DAY_OF_WEEK, dtStart,
739 candidate);
740 break;
741
742 case MONTHLY :
743 reduce_day_of_month(dtStart, candidate);
744 break;
745
746 case YEARLY :
747 reduce_day_of_year(dtStart, candidate);
748 break;
749 }
750
751 return candidate;
752 }
753
754
757 protected static void reduce_constant_length_field(int field,
758 Calendar start,
759 Calendar candidate) {
760 if ((start.getMaximum(field) != start.getLeastMaximum(field))
761 || (start.getMinimum(field) != start.getGreatestMinimum(field))) {
762 throw new IllegalArgumentException("Not a constant length field");
763 }
764
765 int fieldLength = (start.getMaximum(field) - start.getMinimum(field)
766 + 1);
767 int delta = start.get(field) - candidate.get(field);
768
769 if (delta > 0) {
770 delta -= fieldLength;
771 }
772
773 candidate.add(field, delta);
774 }
775
776
779 protected static void reduce_day_of_month(Calendar start,
780 Calendar candidate) {
781 Calendar tempCal = (Calendar)candidate.clone();
782
783 tempCal.add(Calendar.MONTH, -1);
784
785 int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
786
787 if (delta > 0) {
788 delta -= tempCal.getActualMaximum(Calendar.DATE);
789 }
790
791 candidate.add(Calendar.DATE, delta);
792
793 while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
794 tempCal.add(Calendar.MONTH, -1);
795 candidate.add(Calendar.DATE,
796 -tempCal.getActualMaximum(Calendar.DATE));
797 }
798 }
799
800
803 protected static void reduce_day_of_year(Calendar start,
804 Calendar candidate) {
805 if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH))
806 || ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH))
807 && (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
808 candidate.add(Calendar.YEAR, -1);
809 }
810
811
812
813 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
814 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
815
816 while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH))
817 || (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
818 candidate.add(Calendar.YEAR, -1);
819 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
820 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
821 }
822 }
823
824
829 protected int getRecurrenceCount(Calendar candidate) {
830 switch (frequency) {
831
832 case NO_RECURRENCE :
833 return 0;
834
835 case DAILY :
836 return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
837
838 case WEEKLY :
839 Calendar tempCand = (Calendar)candidate.clone();
840
841 tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
842
843 return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
844
845 case MONTHLY :
846 return (int)(getMonthNumber(candidate)
847 - getMonthNumber(dtStart));
848
849 case YEARLY :
850 return candidate.get(Calendar.YEAR)
851 - dtStart.get(Calendar.YEAR);
852
853 default :
854 throw new IllegalStateException("bad frequency internally...");
855 }
856 }
857
858
863 protected static long getDayNumber(Calendar cal) {
864 Calendar tempCal = (Calendar)cal.clone();
865
866
867
868 tempCal.set(Calendar.MILLISECOND, 0);
869 tempCal.set(Calendar.SECOND, 0);
870 tempCal.set(Calendar.MINUTE, 0);
871 tempCal.set(Calendar.HOUR_OF_DAY, 0);
872
873 return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
874 }
875
876
881 protected static long getWeekNumber(Calendar cal) {
882 Calendar tempCal = (Calendar)cal.clone();
883
884
885
886 tempCal.set(Calendar.MILLISECOND, 0);
887 tempCal.set(Calendar.SECOND, 0);
888 tempCal.set(Calendar.MINUTE, 0);
889 tempCal.set(Calendar.HOUR_OF_DAY, 0);
890
891
892
893 int delta = tempCal.getFirstDayOfWeek()
894 - tempCal.get(Calendar.DAY_OF_WEEK);
895
896 if (delta > 0) {
897 delta -= 7;
898 }
899
900
901
902
903
904
905 long weekEpoch = (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24
906 * 60 * 60 * 1000L;
907
908 return (tempCal.getTime().getTime() - weekEpoch)
909 / (7 * 24 * 60 * 60 * 1000);
910 }
911
912
917 protected static long getMonthNumber(Calendar cal) {
918 return (cal.get(Calendar.YEAR) - 1970) * 12
919 + (cal.get(Calendar.MONTH) - Calendar.JANUARY);
920 }
921
922
927 protected boolean matchesByDay(Calendar candidate) {
928 if ((byDay == null) || (byDay.length == 0)) {
929
930
931
932 return true;
933 }
934
935 int i;
936
937 for (i = 0; i < byDay.length; i++) {
938 if (matchesIndividualByDay(candidate, byDay[i])) {
939 return true;
940 }
941 }
942
943 return false;
944 }
945
946
951 protected boolean matchesIndividualByDay(Calendar candidate,
952 DayAndPosition pos) {
953 if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
954 return false;
955 }
956
957 int position = pos.getDayPosition();
958
959 if (position == 0) {
960 return true;
961 }
962
963 int field = Calendar.DAY_OF_MONTH;
964
965 if (position > 0) {
966 int candidatePosition = ((candidate.get(field) - 1) / 7) + 1;
967
968 return (position == candidatePosition);
969 }
970 else {
971
972
973
974 int negativeCandidatePosition =
975 ((candidate.getActualMaximum(field) - candidate.get(field)) / 7)
976 + 1;
977
978 return (-position == negativeCandidatePosition);
979 }
980 }
981
982
987 protected static boolean matchesByField(int[] array, int field,
988 Calendar candidate,
989 boolean allowNegative) {
990 if ((array == null) || (array.length == 0)) {
991
992
993
994 return true;
995 }
996
997 int i;
998
999 for (i = 0; i < array.length; i++) {
1000 int val;
1001
1002 if (allowNegative && (array[i] < 0)) {
1003
1004
1005
1006 int max = candidate.getActualMaximum(field);
1007
1008 val = (max + 1) + array[i];
1009 }
1010 else {
1011 val = array[i];
1012 }
1013
1014 if (val == candidate.get(field)) {
1015 return true;
1016 }
1017 }
1018
1019 return false;
1020 }
1021
1022
1027 protected boolean matchesByMonthDay(Calendar candidate) {
1028 return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
1029 }
1030
1031
1036 protected boolean matchesByYearDay(Calendar candidate) {
1037 return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1038 }
1039
1040
1045 protected boolean matchesByWeekNo(Calendar candidate) {
1046 return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1047 }
1048
1049
1054 protected boolean matchesByMonth(Calendar candidate) {
1055 return matchesByField(byMonth, Calendar.MONTH, candidate, false);
1056 }
1057
1058
1063 public String toString() {
1064 StringBundler sb = new StringBundler();
1065
1066 sb.append(getClass().getName());
1067 sb.append("[dtStart=");
1068 sb.append((dtStart != null) ? dtStart.toString() : "null");
1069 sb.append(",duration=");
1070 sb.append((duration != null) ? duration.toString() : "null");
1071 sb.append(",frequency=");
1072 sb.append(frequency);
1073 sb.append(",interval=");
1074 sb.append(interval);
1075 sb.append(",until=");
1076 sb.append((until != null) ? until.toString() : "null");
1077 sb.append(",byDay=");
1078
1079 if (byDay == null) {
1080 sb.append("null");
1081 }
1082 else {
1083 sb.append("[");
1084
1085 for (int i = 0; i < byDay.length; i++) {
1086 if (i != 0) {
1087 sb.append(",");
1088 }
1089
1090 if (byDay[i] != null) {
1091 sb.append(byDay[i].toString());
1092 }
1093 else {
1094 sb.append("null");
1095 }
1096 }
1097
1098 sb.append("]");
1099 }
1100
1101 sb.append(",byMonthDay=");
1102 sb.append(stringizeIntArray(byMonthDay));
1103 sb.append(",byYearDay=");
1104 sb.append(stringizeIntArray(byYearDay));
1105 sb.append(",byWeekNo=");
1106 sb.append(stringizeIntArray(byWeekNo));
1107 sb.append(",byMonth=");
1108 sb.append(stringizeIntArray(byMonth));
1109 sb.append(']');
1110
1111 return sb.toString();
1112 }
1113
1114
1119 private String stringizeIntArray(int[] a) {
1120 if (a == null) {
1121 return "null";
1122 }
1123
1124 StringBundler sb = new StringBundler(2 * a.length + 1);
1125
1126 sb.append("[");
1127
1128 for (int i = 0; i < a.length; i++) {
1129 if (i != 0) {
1130 sb.append(",");
1131 }
1132
1133 sb.append(a[i]);
1134 }
1135
1136 sb.append("]");
1137
1138 return sb.toString();
1139 }
1140
1141 }