1
14
15
44
45 package com.liferay.util.cal;
46
47 import com.liferay.portal.kernel.util.CalendarFactoryUtil;
48 import com.liferay.portal.kernel.util.StringBundler;
49 import com.liferay.portal.kernel.util.StringPool;
50
51 import java.io.Serializable;
52
53 import java.util.Calendar;
54 import java.util.Date;
55 import java.util.TimeZone;
56
57
64 public class Recurrence implements Serializable {
65
66
69 public final static int DAILY = 3;
70
71
74 public final static int WEEKLY = 4;
75
76
79 public final static int MONTHLY = 5;
80
81
84 public final static int YEARLY = 6;
85
86
89 public final static int NO_RECURRENCE = 7;
90
91
94 protected Calendar dtStart;
95
96
99 protected Duration duration;
100
101
104 protected int frequency;
105
106
109 protected int interval;
110
111
114 protected int occurrence = 0;
115
116
119 protected Calendar until;
120
121
124 protected DayAndPosition[] byDay;
125
126
129 protected int[] byMonthDay;
130
131
134 protected int[] byYearDay;
135
136
139 protected int[] byWeekNo;
140
141
144 protected int[] byMonth;
145
146
149 public Recurrence() {
150 this(null, new Duration(), NO_RECURRENCE);
151 }
152
153
156 public Recurrence(Calendar start, Duration dur) {
157 this(start, dur, NO_RECURRENCE);
158 }
159
160
163 public Recurrence(Calendar start, Duration dur, int freq) {
164 setDtStart(start);
165
166 duration = (Duration)dur.clone();
167 frequency = freq;
168 interval = 1;
169 }
170
171
172
173
178 public Calendar getDtStart() {
179 return (Calendar)dtStart.clone();
180 }
181
182
185 public void setDtStart(Calendar start) {
186 int oldStart;
187
188 if (dtStart != null) {
189 oldStart = dtStart.getFirstDayOfWeek();
190 }
191 else {
192 oldStart = Calendar.MONDAY;
193 }
194
195 if (start == null) {
196 dtStart = CalendarFactoryUtil.getCalendar(
197 TimeZone.getTimeZone(StringPool.UTC));
198
199 dtStart.setTime(new Date(0L));
200 }
201 else {
202 dtStart = (Calendar)start.clone();
203
204 dtStart.clear(Calendar.ZONE_OFFSET);
205 dtStart.clear(Calendar.DST_OFFSET);
206 dtStart.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
207 }
208
209 dtStart.setMinimalDaysInFirstWeek(4);
210 dtStart.setFirstDayOfWeek(oldStart);
211 }
212
213
218 public Duration getDuration() {
219 return (Duration)duration.clone();
220 }
221
222
225 public void setDuration(Duration d) {
226 duration = (Duration)d.clone();
227 }
228
229
234 public Calendar getDtEnd() {
235
236
240 Calendar tempEnd = (Calendar)dtStart.clone();
241
242 tempEnd.setTime(new Date(dtStart.getTime().getTime()
243 + duration.getInterval()));
244
245 return tempEnd;
246 }
247
248
251 public void setDtEnd(Calendar end) {
252 Calendar tempEnd = (Calendar)end.clone();
253
254 tempEnd.clear(Calendar.ZONE_OFFSET);
255 tempEnd.clear(Calendar.DST_OFFSET);
256 tempEnd.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
257 duration.setInterval(tempEnd.getTime().getTime()
258 - dtStart.getTime().getTime());
259 }
260
261
266 public int getFrequency() {
267 return frequency;
268 }
269
270
273 public void setFrequency(int freq) {
274 if ((frequency != DAILY) && (frequency != WEEKLY)
275 && (frequency != MONTHLY) && (frequency != YEARLY)
276 && (frequency != NO_RECURRENCE)) {
277 throw new IllegalArgumentException("Invalid frequency");
278 }
279
280 frequency = freq;
281 }
282
283
288 public int getInterval() {
289 return interval;
290 }
291
292
295 public void setInterval(int intr) {
296 interval = (intr > 0) ? intr : 1;
297 }
298
299
304 public int getOccurrence() {
305 return occurrence;
306 }
307
308
311 public void setOccurrence(int occur) {
312 occurrence = occur;
313 }
314
315
320 public Calendar getUntil() {
321 return ((until != null) ? (Calendar)until.clone() : null);
322 }
323
324
327 public void setUntil(Calendar u) {
328 if (u == null) {
329 until = null;
330
331 return;
332 }
333
334 until = (Calendar)u.clone();
335
336 until.clear(Calendar.ZONE_OFFSET);
337 until.clear(Calendar.DST_OFFSET);
338 until.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
339 }
340
341
346 public int getWeekStart() {
347 return dtStart.getFirstDayOfWeek();
348 }
349
350
353 public void setWeekStart(int weekstart) {
354 dtStart.setFirstDayOfWeek(weekstart);
355 }
356
357
362 public DayAndPosition[] getByDay() {
363 if (byDay == null) {
364 return null;
365 }
366
367 DayAndPosition[] b = new DayAndPosition[byDay.length];
368
369
373 for (int i = 0; i < byDay.length; i++) {
374 b[i] = (DayAndPosition)byDay[i].clone();
375 }
376
377 return b;
378 }
379
380
383 public void setByDay(DayAndPosition[] b) {
384 if (b == null) {
385 byDay = null;
386
387 return;
388 }
389
390 byDay = new DayAndPosition[b.length];
391
392
396 for (int i = 0; i < b.length; i++) {
397 byDay[i] = (DayAndPosition)b[i].clone();
398 }
399 }
400
401
406 public int[] getByMonthDay() {
407 if (byMonthDay == null) {
408 return null;
409 }
410
411 int[] b = new int[byMonthDay.length];
412
413 System.arraycopy(byMonthDay, 0, b, 0, byMonthDay.length);
414
415 return b;
416 }
417
418
421 public void setByMonthDay(int[] b) {
422 if (b == null) {
423 byMonthDay = null;
424
425 return;
426 }
427
428 byMonthDay = new int[b.length];
429
430 System.arraycopy(b, 0, byMonthDay, 0, b.length);
431 }
432
433
438 public int[] getByYearDay() {
439 if (byYearDay == null) {
440 return null;
441 }
442
443 int[] b = new int[byYearDay.length];
444
445 System.arraycopy(byYearDay, 0, b, 0, byYearDay.length);
446
447 return b;
448 }
449
450
453 public void setByYearDay(int[] b) {
454 if (b == null) {
455 byYearDay = null;
456
457 return;
458 }
459
460 byYearDay = new int[b.length];
461
462 System.arraycopy(b, 0, byYearDay, 0, b.length);
463 }
464
465
470 public int[] getByWeekNo() {
471 if (byWeekNo == null) {
472 return null;
473 }
474
475 int[] b = new int[byWeekNo.length];
476
477 System.arraycopy(byWeekNo, 0, b, 0, byWeekNo.length);
478
479 return b;
480 }
481
482
485 public void setByWeekNo(int[] b) {
486 if (b == null) {
487 byWeekNo = null;
488
489 return;
490 }
491
492 byWeekNo = new int[b.length];
493
494 System.arraycopy(b, 0, byWeekNo, 0, b.length);
495 }
496
497
502 public int[] getByMonth() {
503 if (byMonth == null) {
504 return null;
505 }
506
507 int[] b = new int[byMonth.length];
508
509 System.arraycopy(byMonth, 0, b, 0, byMonth.length);
510
511 return b;
512 }
513
514
517 public void setByMonth(int[] b) {
518 if (b == null) {
519 byMonth = null;
520
521 return;
522 }
523
524 byMonth = new int[b.length];
525
526 System.arraycopy(b, 0, byMonth, 0, b.length);
527 }
528
529
534 public boolean isInRecurrence(Calendar current) {
535 return isInRecurrence(current, false);
536 }
537
538
543 public boolean isInRecurrence(Calendar current, boolean debug) {
544 Calendar myCurrent = (Calendar)current.clone();
545
546
548 myCurrent.clear(Calendar.ZONE_OFFSET);
549 myCurrent.clear(Calendar.DST_OFFSET);
550 myCurrent.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
551 myCurrent.setMinimalDaysInFirstWeek(4);
552 myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
553
554 if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
555
556
558 if (debug) {
559 System.err.println("current < start");
560 }
561
562 return false;
563 }
564
565 if (myCurrent.getTime().getTime()
566 < dtStart.getTime().getTime() + duration.getInterval()) {
567
568
570 if (debug) {
571 System.err.println("within duration of start");
572 }
573
574 return true;
575 }
576
577 Calendar candidate = getCandidateStartTime(myCurrent);
578
579
580
581 while (candidate.getTime().getTime() + duration.getInterval()
582 > myCurrent.getTime().getTime()) {
583 if (candidateIsInRecurrence(candidate, debug)) {
584 return true;
585 }
586
587
588
589 candidate.add(Calendar.SECOND, -1);
590
591
592
593 if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
594 if (debug) {
595 System.err.println("No candidates after dtStart");
596 }
597
598 return false;
599 }
600
601 candidate = getCandidateStartTime(candidate);
602 }
603
604 if (debug) {
605 System.err.println("No matching candidates");
606 }
607
608 return false;
609 }
610
611
616 protected boolean candidateIsInRecurrence(Calendar candidate,
617 boolean debug) {
618 if ((until != null)
619 && (candidate.getTime().getTime() > until.getTime().getTime())) {
620
621
623 if (debug) {
624 System.err.println("after until");
625 }
626
627 return false;
628 }
629
630 if (getRecurrenceCount(candidate) % interval != 0) {
631
632
634 if (debug) {
635 System.err.println("not an interval rep");
636 }
637
638 return false;
639 }
640 else if ((occurrence > 0) &&
641 (getRecurrenceCount(candidate) >= occurrence)) {
642
643 return false;
644 }
645
646 if (!matchesByDay(candidate) ||!matchesByMonthDay(candidate)
647 ||!matchesByYearDay(candidate) ||!matchesByWeekNo(candidate)
648 ||!matchesByMonth(candidate)) {
649
650
652 if (debug) {
653 System.err.println("doesn't match a by*");
654 }
655
656 return false;
657 }
658
659 if (debug) {
660 System.err.println("All checks succeeded");
661 }
662
663 return true;
664 }
665
666
671 protected int getMinimumInterval() {
672 if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null)
673 || (byYearDay != null)) {
674 return DAILY;
675 }
676 else if ((frequency == WEEKLY) || (byWeekNo != null)) {
677 return WEEKLY;
678 }
679 else if ((frequency == MONTHLY) || (byMonth != null)) {
680 return MONTHLY;
681 }
682 else if (frequency == YEARLY) {
683 return YEARLY;
684 }
685 else if (frequency == NO_RECURRENCE) {
686 return NO_RECURRENCE;
687 }
688 else {
689
690
692 throw new IllegalStateException(
693 "Internal error: Unknown frequency value");
694 }
695 }
696
697
702 public Calendar getCandidateStartTime(Calendar current) {
703 if (dtStart.getTime().getTime() > current.getTime().getTime()) {
704 throw new IllegalArgumentException("Current time before DtStart");
705 }
706
707 int minInterval = getMinimumInterval();
708 Calendar candidate = (Calendar)current.clone();
709
710 if (true) {
711
712
714 candidate.clear(Calendar.ZONE_OFFSET);
715 candidate.clear(Calendar.DST_OFFSET);
716 candidate.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
717 candidate.setMinimalDaysInFirstWeek(4);
718 candidate.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
719 }
720
721 if (frequency == NO_RECURRENCE) {
722 candidate.setTime(dtStart.getTime());
723
724 return candidate;
725 }
726
727 reduce_constant_length_field(Calendar.SECOND, dtStart, candidate);
728 reduce_constant_length_field(Calendar.MINUTE, dtStart, candidate);
729 reduce_constant_length_field(Calendar.HOUR_OF_DAY, dtStart, candidate);
730
731 switch (minInterval) {
732
733 case DAILY :
734
735
736
737 break;
738
739 case WEEKLY :
740 reduce_constant_length_field(Calendar.DAY_OF_WEEK, dtStart,
741 candidate);
742 break;
743
744 case MONTHLY :
745 reduce_day_of_month(dtStart, candidate);
746 break;
747
748 case YEARLY :
749 reduce_day_of_year(dtStart, candidate);
750 break;
751 }
752
753 return candidate;
754 }
755
756
759 protected static void reduce_constant_length_field(int field,
760 Calendar start,
761 Calendar candidate) {
762 if ((start.getMaximum(field) != start.getLeastMaximum(field))
763 || (start.getMinimum(field) != start.getGreatestMinimum(field))) {
764 throw new IllegalArgumentException("Not a constant length field");
765 }
766
767 int fieldLength = (start.getMaximum(field) - start.getMinimum(field)
768 + 1);
769 int delta = start.get(field) - candidate.get(field);
770
771 if (delta > 0) {
772 delta -= fieldLength;
773 }
774
775 candidate.add(field, delta);
776 }
777
778
781 protected static void reduce_day_of_month(Calendar start,
782 Calendar candidate) {
783 Calendar tempCal = (Calendar)candidate.clone();
784
785 tempCal.add(Calendar.MONTH, -1);
786
787 int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
788
789 if (delta > 0) {
790 delta -= tempCal.getActualMaximum(Calendar.DATE);
791 }
792
793 candidate.add(Calendar.DATE, delta);
794
795 while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
796 tempCal.add(Calendar.MONTH, -1);
797 candidate.add(Calendar.DATE,
798 -tempCal.getActualMaximum(Calendar.DATE));
799 }
800 }
801
802
805 protected static void reduce_day_of_year(Calendar start,
806 Calendar candidate) {
807 if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH))
808 || ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH))
809 && (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
810 candidate.add(Calendar.YEAR, -1);
811 }
812
813
814
815 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
816 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
817
818 while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH))
819 || (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
820 candidate.add(Calendar.YEAR, -1);
821 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
822 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
823 }
824 }
825
826
831 protected int getRecurrenceCount(Calendar candidate) {
832 switch (frequency) {
833
834 case NO_RECURRENCE :
835 return 0;
836
837 case DAILY :
838 return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
839
840 case WEEKLY :
841 Calendar tempCand = (Calendar)candidate.clone();
842
843 tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
844
845 return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
846
847 case MONTHLY :
848 return (int)(getMonthNumber(candidate)
849 - getMonthNumber(dtStart));
850
851 case YEARLY :
852 return candidate.get(Calendar.YEAR)
853 - dtStart.get(Calendar.YEAR);
854
855 default :
856 throw new IllegalStateException("bad frequency internally...");
857 }
858 }
859
860
865 protected static long getDayNumber(Calendar cal) {
866 Calendar tempCal = (Calendar)cal.clone();
867
868
870 tempCal.set(Calendar.MILLISECOND, 0);
871 tempCal.set(Calendar.SECOND, 0);
872 tempCal.set(Calendar.MINUTE, 0);
873 tempCal.set(Calendar.HOUR_OF_DAY, 0);
874
875 return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
876 }
877
878
883 protected static long getWeekNumber(Calendar cal) {
884 Calendar tempCal = (Calendar)cal.clone();
885
886
888 tempCal.set(Calendar.MILLISECOND, 0);
889 tempCal.set(Calendar.SECOND, 0);
890 tempCal.set(Calendar.MINUTE, 0);
891 tempCal.set(Calendar.HOUR_OF_DAY, 0);
892
893
895 int delta = tempCal.getFirstDayOfWeek()
896 - tempCal.get(Calendar.DAY_OF_WEEK);
897
898 if (delta > 0) {
899 delta -= 7;
900 }
901
902
904
907 long weekEpoch = (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24
908 * 60 * 60 * 1000L;
909
910 return (tempCal.getTime().getTime() - weekEpoch)
911 / (7 * 24 * 60 * 60 * 1000);
912 }
913
914
919 protected static long getMonthNumber(Calendar cal) {
920 return (cal.get(Calendar.YEAR) - 1970) * 12
921 + (cal.get(Calendar.MONTH) - Calendar.JANUARY);
922 }
923
924
929 protected boolean matchesByDay(Calendar candidate) {
930 if ((byDay == null) || (byDay.length == 0)) {
931
932
933
934 return true;
935 }
936
937 int i;
938
939 for (i = 0; i < byDay.length; i++) {
940 if (matchesIndividualByDay(candidate, byDay[i])) {
941 return true;
942 }
943 }
944
945 return false;
946 }
947
948
953 protected boolean matchesIndividualByDay(Calendar candidate,
954 DayAndPosition pos) {
955 if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
956 return false;
957 }
958
959 int position = pos.getDayPosition();
960
961 if (position == 0) {
962 return true;
963 }
964
965 int field;
966
967 switch (frequency) {
968
969 case MONTHLY :
970 field = Calendar.DAY_OF_MONTH;
971 break;
972
973 case YEARLY :
974 field = Calendar.DAY_OF_YEAR;
975 break;
976
977 default :
978 throw new IllegalStateException(
979 "byday has a day position "
980 + "in non-MONTHLY or YEARLY recurrence");
981 }
982
983 if (position > 0) {
984 int day_of_week_in_field = ((candidate.get(field) - 1) / 7) + 1;
985
986 return (position == day_of_week_in_field);
987 }
988 else {
989
990
991
992 int negative_day_of_week_in_field =
993 ((candidate.getActualMaximum(field) - candidate.get(field)) / 7)
994 + 1;
995
996 return (-position == negative_day_of_week_in_field);
997 }
998 }
999
1000
1005 protected static boolean matchesByField(int[] array, int field,
1006 Calendar candidate,
1007 boolean allowNegative) {
1008 if ((array == null) || (array.length == 0)) {
1009
1010
1011
1012 return true;
1013 }
1014
1015 int i;
1016
1017 for (i = 0; i < array.length; i++) {
1018 int val;
1019
1020 if (allowNegative && (array[i] < 0)) {
1021
1022
1024 int max = candidate.getActualMaximum(field);
1025
1026 val = (max + 1) + array[i];
1027 }
1028 else {
1029 val = array[i];
1030 }
1031
1032 if (val == candidate.get(field)) {
1033 return true;
1034 }
1035 }
1036
1037 return false;
1038 }
1039
1040
1045 protected boolean matchesByMonthDay(Calendar candidate) {
1046 return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
1047 }
1048
1049
1054 protected boolean matchesByYearDay(Calendar candidate) {
1055 return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1056 }
1057
1058
1063 protected boolean matchesByWeekNo(Calendar candidate) {
1064 return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1065 }
1066
1067
1072 protected boolean matchesByMonth(Calendar candidate) {
1073 return matchesByField(byMonth, Calendar.MONTH, candidate, false);
1074 }
1075
1076
1081 public String toString() {
1082 StringBundler sb = new StringBundler();
1083
1084 sb.append(getClass().getName());
1085 sb.append("[dtStart=");
1086 sb.append((dtStart != null) ? dtStart.toString() : "null");
1087 sb.append(",duration=");
1088 sb.append((duration != null) ? duration.toString() : "null");
1089 sb.append(",frequency=");
1090 sb.append(frequency);
1091 sb.append(",interval=");
1092 sb.append(interval);
1093 sb.append(",until=");
1094 sb.append((until != null) ? until.toString() : "null");
1095 sb.append(",byDay=");
1096
1097 if (byDay == null) {
1098 sb.append("null");
1099 }
1100 else {
1101 sb.append("[");
1102
1103 for (int i = 0; i < byDay.length; i++) {
1104 if (i != 0) {
1105 sb.append(",");
1106 }
1107
1108 if (byDay[i] != null) {
1109 sb.append(byDay[i].toString());
1110 }
1111 else {
1112 sb.append("null");
1113 }
1114 }
1115
1116 sb.append("]");
1117 }
1118
1119 sb.append(",byMonthDay=");
1120 sb.append(stringizeIntArray(byMonthDay));
1121 sb.append(",byYearDay=");
1122 sb.append(stringizeIntArray(byYearDay));
1123 sb.append(",byWeekNo=");
1124 sb.append(stringizeIntArray(byWeekNo));
1125 sb.append(",byMonth=");
1126 sb.append(stringizeIntArray(byMonth));
1127 sb.append(']');
1128
1129 return sb.toString();
1130 }
1131
1132
1137 private String stringizeIntArray(int[] a) {
1138 if (a == null) {
1139 return "null";
1140 }
1141
1142 StringBundler sb = new StringBundler(2 * a.length + 1);
1143
1144 sb.append("[");
1145
1146 for (int i = 0; i < a.length; i++) {
1147 if (i != 0) {
1148 sb.append(",");
1149 }
1150
1151 sb.append(a[i]);
1152 }
1153
1154 sb.append("]");
1155
1156 return sb.toString();
1157 }
1158
1159}