1
22
23
52
53 package com.liferay.util.cal;
54
55 import com.liferay.portal.kernel.util.CalendarFactoryUtil;
56 import com.liferay.portal.kernel.util.StringPool;
57
58 import java.io.Serializable;
59
60 import java.util.Calendar;
61 import java.util.Date;
62 import java.util.TimeZone;
63
64
73 public class Recurrence implements Serializable {
74
75
78 public final static int DAILY = 3;
79
80
83 public final static int WEEKLY = 4;
84
85
88 public final static int MONTHLY = 5;
89
90
93 public final static int YEARLY = 6;
94
95
98 public final static int NO_RECURRENCE = 7;
99
100
103 protected Calendar dtStart;
104
105
108 protected Duration duration;
109
110
113 protected int frequency;
114
115
118 protected int interval;
119
120
123 protected int occurrence = 0;
124
125
128 protected Calendar until;
129
130
133 protected DayAndPosition[] byDay;
134
135
138 protected int[] byMonthDay;
139
140
143 protected int[] byYearDay;
144
145
148 protected int[] byWeekNo;
149
150
153 protected int[] byMonth;
154
155
160 public Recurrence() {
161 this(null, new Duration(), NO_RECURRENCE);
162 }
163
164
172 public Recurrence(Calendar start, Duration dur) {
173 this(start, dur, NO_RECURRENCE);
174 }
175
176
185 public Recurrence(Calendar start, Duration dur, int freq) {
186 setDtStart(start);
187
188 duration = (Duration)dur.clone();
189 frequency = freq;
190 interval = 1;
191 }
192
193
194
195
202 public Calendar getDtStart() {
203 return (Calendar)dtStart.clone();
204 }
205
206
213 public void setDtStart(Calendar start) {
214 int oldStart;
215
216 if (dtStart != null) {
217 oldStart = dtStart.getFirstDayOfWeek();
218 }
219 else {
220 oldStart = Calendar.MONDAY;
221 }
222
223 if (start == null) {
224 dtStart = CalendarFactoryUtil.getCalendar(
225 TimeZone.getTimeZone(StringPool.UTC));
226
227 dtStart.setTime(new Date(0L));
228 }
229 else {
230 dtStart = (Calendar)start.clone();
231
232 dtStart.clear(Calendar.ZONE_OFFSET);
233 dtStart.clear(Calendar.DST_OFFSET);
234 dtStart.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
235 }
236
237 dtStart.setMinimalDaysInFirstWeek(4);
238 dtStart.setFirstDayOfWeek(oldStart);
239 }
240
241
248 public Duration getDuration() {
249 return (Duration)duration.clone();
250 }
251
252
259 public void setDuration(Duration d) {
260 duration = (Duration)d.clone();
261 }
262
263
270 public Calendar getDtEnd() {
271
272
276 Calendar tempEnd = (Calendar)dtStart.clone();
277
278 tempEnd.setTime(new Date(dtStart.getTime().getTime()
279 + duration.getInterval()));
280
281 return tempEnd;
282 }
283
284
291 public void setDtEnd(Calendar end) {
292 Calendar tempEnd = (Calendar)end.clone();
293
294 tempEnd.clear(Calendar.ZONE_OFFSET);
295 tempEnd.clear(Calendar.DST_OFFSET);
296 tempEnd.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
297 duration.setInterval(tempEnd.getTime().getTime()
298 - dtStart.getTime().getTime());
299 }
300
301
308 public int getFrequency() {
309 return frequency;
310 }
311
312
319 public void setFrequency(int freq) {
320 if ((frequency != DAILY) && (frequency != WEEKLY)
321 && (frequency != MONTHLY) && (frequency != YEARLY)
322 && (frequency != NO_RECURRENCE)) {
323 throw new IllegalArgumentException("Invalid frequency");
324 }
325
326 frequency = freq;
327 }
328
329
336 public int getInterval() {
337 return interval;
338 }
339
340
347 public void setInterval(int intr) {
348 interval = (intr > 0) ? intr : 1;
349 }
350
351
358 public int getOccurrence() {
359 return occurrence;
360 }
361
362
369 public void setOccurrence(int occur) {
370 occurrence = occur;
371 }
372
373
380 public Calendar getUntil() {
381 return ((until != null) ? (Calendar)until.clone() : null);
382 }
383
384
391 public void setUntil(Calendar u) {
392 if (u == null) {
393 until = null;
394
395 return;
396 }
397
398 until = (Calendar)u.clone();
399
400 until.clear(Calendar.ZONE_OFFSET);
401 until.clear(Calendar.DST_OFFSET);
402 until.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
403 }
404
405
412 public int getWeekStart() {
413 return dtStart.getFirstDayOfWeek();
414 }
415
416
423 public void setWeekStart(int weekstart) {
424 dtStart.setFirstDayOfWeek(weekstart);
425 }
426
427
434 public DayAndPosition[] getByDay() {
435 if (byDay == null) {
436 return null;
437 }
438
439 DayAndPosition[] b = new DayAndPosition[byDay.length];
440
441
445 for (int i = 0; i < byDay.length; i++) {
446 b[i] = (DayAndPosition)byDay[i].clone();
447 }
448
449 return b;
450 }
451
452
459 public void setByDay(DayAndPosition[] b) {
460 if (b == null) {
461 byDay = null;
462
463 return;
464 }
465
466 byDay = new DayAndPosition[b.length];
467
468
472 for (int i = 0; i < b.length; i++) {
473 byDay[i] = (DayAndPosition)b[i].clone();
474 }
475 }
476
477
484 public int[] getByMonthDay() {
485 if (byMonthDay == null) {
486 return null;
487 }
488
489 int[] b = new int[byMonthDay.length];
490
491 System.arraycopy(byMonthDay, 0, b, 0, byMonthDay.length);
492
493 return b;
494 }
495
496
503 public void setByMonthDay(int[] b) {
504 if (b == null) {
505 byMonthDay = null;
506
507 return;
508 }
509
510 byMonthDay = new int[b.length];
511
512 System.arraycopy(b, 0, byMonthDay, 0, b.length);
513 }
514
515
522 public int[] getByYearDay() {
523 if (byYearDay == null) {
524 return null;
525 }
526
527 int[] b = new int[byYearDay.length];
528
529 System.arraycopy(byYearDay, 0, b, 0, byYearDay.length);
530
531 return b;
532 }
533
534
541 public void setByYearDay(int[] b) {
542 if (b == null) {
543 byYearDay = null;
544
545 return;
546 }
547
548 byYearDay = new int[b.length];
549
550 System.arraycopy(b, 0, byYearDay, 0, b.length);
551 }
552
553
560 public int[] getByWeekNo() {
561 if (byWeekNo == null) {
562 return null;
563 }
564
565 int[] b = new int[byWeekNo.length];
566
567 System.arraycopy(byWeekNo, 0, b, 0, byWeekNo.length);
568
569 return b;
570 }
571
572
579 public void setByWeekNo(int[] b) {
580 if (b == null) {
581 byWeekNo = null;
582
583 return;
584 }
585
586 byWeekNo = new int[b.length];
587
588 System.arraycopy(b, 0, byWeekNo, 0, b.length);
589 }
590
591
598 public int[] getByMonth() {
599 if (byMonth == null) {
600 return null;
601 }
602
603 int[] b = new int[byMonth.length];
604
605 System.arraycopy(byMonth, 0, b, 0, byMonth.length);
606
607 return b;
608 }
609
610
617 public void setByMonth(int[] b) {
618 if (b == null) {
619 byMonth = null;
620
621 return;
622 }
623
624 byMonth = new int[b.length];
625
626 System.arraycopy(b, 0, byMonth, 0, b.length);
627 }
628
629
638 public boolean isInRecurrence(Calendar current) {
639 return isInRecurrence(current, false);
640 }
641
642
652 public boolean isInRecurrence(Calendar current, boolean debug) {
653 Calendar myCurrent = (Calendar)current.clone();
654
655
657 myCurrent.clear(Calendar.ZONE_OFFSET);
658 myCurrent.clear(Calendar.DST_OFFSET);
659 myCurrent.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
660 myCurrent.setMinimalDaysInFirstWeek(4);
661 myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
662
663 if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
664
665
667 if (debug) {
668 System.err.println("current < start");
669 }
670
671 return false;
672 }
673
674 if (myCurrent.getTime().getTime()
675 < dtStart.getTime().getTime() + duration.getInterval()) {
676
677
679 if (debug) {
680 System.err.println("within duration of start");
681 }
682
683 return true;
684 }
685
686 Calendar candidate = getCandidateStartTime(myCurrent);
687
688
689
690 while (candidate.getTime().getTime() + duration.getInterval()
691 > myCurrent.getTime().getTime()) {
692 if (candidateIsInRecurrence(candidate, debug)) {
693 return true;
694 }
695
696
697
698 candidate.add(Calendar.SECOND, -1);
699
700
701
702 if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
703 if (debug) {
704 System.err.println("No candidates after dtStart");
705 }
706
707 return false;
708 }
709
710 candidate = getCandidateStartTime(candidate);
711 }
712
713 if (debug) {
714 System.err.println("No matching candidates");
715 }
716
717 return false;
718 }
719
720
730 protected boolean candidateIsInRecurrence(Calendar candidate,
731 boolean debug) {
732 if ((until != null)
733 && (candidate.getTime().getTime() > until.getTime().getTime())) {
734
735
737 if (debug) {
738 System.err.println("after until");
739 }
740
741 return false;
742 }
743
744 if (getRecurrenceCount(candidate) % interval != 0) {
745
746
748 if (debug) {
749 System.err.println("not an interval rep");
750 }
751
752 return false;
753 }
754 else if ((occurrence > 0) &&
755 (getRecurrenceCount(candidate) >= occurrence)) {
756
757 return false;
758 }
759
760 if (!matchesByDay(candidate) ||!matchesByMonthDay(candidate)
761 ||!matchesByYearDay(candidate) ||!matchesByWeekNo(candidate)
762 ||!matchesByMonth(candidate)) {
763
764
766 if (debug) {
767 System.err.println("doesn't match a by*");
768 }
769
770 return false;
771 }
772
773 if (debug) {
774 System.err.println("All checks succeeded");
775 }
776
777 return true;
778 }
779
780
787 protected int getMinimumInterval() {
788 if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null)
789 || (byYearDay != null)) {
790 return DAILY;
791 }
792 else if ((frequency == WEEKLY) || (byWeekNo != null)) {
793 return WEEKLY;
794 }
795 else if ((frequency == MONTHLY) || (byMonth != null)) {
796 return MONTHLY;
797 }
798 else if (frequency == YEARLY) {
799 return YEARLY;
800 }
801 else if (frequency == NO_RECURRENCE) {
802 return NO_RECURRENCE;
803 }
804 else {
805
806
808 throw new IllegalStateException(
809 "Internal error: Unknown frequency value");
810 }
811 }
812
813
822 public Calendar getCandidateStartTime(Calendar current) {
823 if (dtStart.getTime().getTime() > current.getTime().getTime()) {
824 throw new IllegalArgumentException("Current time before DtStart");
825 }
826
827 int minInterval = getMinimumInterval();
828 Calendar candidate = (Calendar)current.clone();
829
830 if (true) {
831
832
834 candidate.clear(Calendar.ZONE_OFFSET);
835 candidate.clear(Calendar.DST_OFFSET);
836 candidate.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
837 candidate.setMinimalDaysInFirstWeek(4);
838 candidate.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
839 }
840
841 if (frequency == NO_RECURRENCE) {
842 candidate.setTime(dtStart.getTime());
843
844 return candidate;
845 }
846
847 reduce_constant_length_field(Calendar.SECOND, dtStart, candidate);
848 reduce_constant_length_field(Calendar.MINUTE, dtStart, candidate);
849 reduce_constant_length_field(Calendar.HOUR_OF_DAY, dtStart, candidate);
850
851 switch (minInterval) {
852
853 case DAILY :
854
855
856
857 break;
858
859 case WEEKLY :
860 reduce_constant_length_field(Calendar.DAY_OF_WEEK, dtStart,
861 candidate);
862 break;
863
864 case MONTHLY :
865 reduce_day_of_month(dtStart, candidate);
866 break;
867
868 case YEARLY :
869 reduce_day_of_year(dtStart, candidate);
870 break;
871 }
872
873 return candidate;
874 }
875
876
885 protected static void reduce_constant_length_field(int field,
886 Calendar start,
887 Calendar candidate) {
888 if ((start.getMaximum(field) != start.getLeastMaximum(field))
889 || (start.getMinimum(field) != start.getGreatestMinimum(field))) {
890 throw new IllegalArgumentException("Not a constant length field");
891 }
892
893 int fieldLength = (start.getMaximum(field) - start.getMinimum(field)
894 + 1);
895 int delta = start.get(field) - candidate.get(field);
896
897 if (delta > 0) {
898 delta -= fieldLength;
899 }
900
901 candidate.add(field, delta);
902 }
903
904
912 protected static void reduce_day_of_month(Calendar start,
913 Calendar candidate) {
914 Calendar tempCal = (Calendar)candidate.clone();
915
916 tempCal.add(Calendar.MONTH, -1);
917
918 int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
919
920 if (delta > 0) {
921 delta -= tempCal.getActualMaximum(Calendar.DATE);
922 }
923
924 candidate.add(Calendar.DATE, delta);
925
926 while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
927 tempCal.add(Calendar.MONTH, -1);
928 candidate.add(Calendar.DATE,
929 -tempCal.getActualMaximum(Calendar.DATE));
930 }
931 }
932
933
941 protected static void reduce_day_of_year(Calendar start,
942 Calendar candidate) {
943 if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH))
944 || ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH))
945 && (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
946 candidate.add(Calendar.YEAR, -1);
947 }
948
949
950
951 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
952 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
953
954 while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH))
955 || (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
956 candidate.add(Calendar.YEAR, -1);
957 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
958 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
959 }
960 }
961
962
971 protected int getRecurrenceCount(Calendar candidate) {
972 switch (frequency) {
973
974 case NO_RECURRENCE :
975 return 0;
976
977 case DAILY :
978 return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
979
980 case WEEKLY :
981 Calendar tempCand = (Calendar)candidate.clone();
982
983 tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
984
985 return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
986
987 case MONTHLY :
988 return (int)(getMonthNumber(candidate)
989 - getMonthNumber(dtStart));
990
991 case YEARLY :
992 return candidate.get(Calendar.YEAR)
993 - dtStart.get(Calendar.YEAR);
994
995 default :
996 throw new IllegalStateException("bad frequency internally...");
997 }
998 }
999
1000
1009 protected static long getDayNumber(Calendar cal) {
1010 Calendar tempCal = (Calendar)cal.clone();
1011
1012
1014 tempCal.set(Calendar.MILLISECOND, 0);
1015 tempCal.set(Calendar.SECOND, 0);
1016 tempCal.set(Calendar.MINUTE, 0);
1017 tempCal.set(Calendar.HOUR_OF_DAY, 0);
1018
1019 return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
1020 }
1021
1022
1031 protected static long getWeekNumber(Calendar cal) {
1032 Calendar tempCal = (Calendar)cal.clone();
1033
1034
1036 tempCal.set(Calendar.MILLISECOND, 0);
1037 tempCal.set(Calendar.SECOND, 0);
1038 tempCal.set(Calendar.MINUTE, 0);
1039 tempCal.set(Calendar.HOUR_OF_DAY, 0);
1040
1041
1043 int delta = tempCal.getFirstDayOfWeek()
1044 - tempCal.get(Calendar.DAY_OF_WEEK);
1045
1046 if (delta > 0) {
1047 delta -= 7;
1048 }
1049
1050
1052
1055 long weekEpoch = (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24
1056 * 60 * 60 * 1000L;
1057
1058 return (tempCal.getTime().getTime() - weekEpoch)
1059 / (7 * 24 * 60 * 60 * 1000);
1060 }
1061
1062
1071 protected static long getMonthNumber(Calendar cal) {
1072 return (cal.get(Calendar.YEAR) - 1970) * 12
1073 + (cal.get(Calendar.MONTH) - Calendar.JANUARY);
1074 }
1075
1076
1085 protected boolean matchesByDay(Calendar candidate) {
1086 if ((byDay == null) || (byDay.length == 0)) {
1087
1088
1089
1090 return true;
1091 }
1092
1093 int i;
1094
1095 for (i = 0; i < byDay.length; i++) {
1096 if (matchesIndividualByDay(candidate, byDay[i])) {
1097 return true;
1098 }
1099 }
1100
1101 return false;
1102 }
1103
1104
1114 protected boolean matchesIndividualByDay(Calendar candidate,
1115 DayAndPosition pos) {
1116 if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
1117 return false;
1118 }
1119
1120 int position = pos.getDayPosition();
1121
1122 if (position == 0) {
1123 return true;
1124 }
1125
1126 int field;
1127
1128 switch (frequency) {
1129
1130 case MONTHLY :
1131 field = Calendar.DAY_OF_MONTH;
1132 break;
1133
1134 case YEARLY :
1135 field = Calendar.DAY_OF_YEAR;
1136 break;
1137
1138 default :
1139 throw new IllegalStateException(
1140 "byday has a day position "
1141 + "in non-MONTHLY or YEARLY recurrence");
1142 }
1143
1144 if (position > 0) {
1145 int day_of_week_in_field = ((candidate.get(field) - 1) / 7) + 1;
1146
1147 return (position == day_of_week_in_field);
1148 }
1149 else {
1150
1151
1152
1153 int negative_day_of_week_in_field =
1154 ((candidate.getActualMaximum(field) - candidate.get(field)) / 7)
1155 + 1;
1156
1157 return (-position == negative_day_of_week_in_field);
1158 }
1159 }
1160
1161
1173 protected static boolean matchesByField(int[] array, int field,
1174 Calendar candidate,
1175 boolean allowNegative) {
1176 if ((array == null) || (array.length == 0)) {
1177
1178
1179
1180 return true;
1181 }
1182
1183 int i;
1184
1185 for (i = 0; i < array.length; i++) {
1186 int val;
1187
1188 if (allowNegative && (array[i] < 0)) {
1189
1190
1192 int max = candidate.getActualMaximum(field);
1193
1194 val = (max + 1) + array[i];
1195 }
1196 else {
1197 val = array[i];
1198 }
1199
1200 if (val == candidate.get(field)) {
1201 return true;
1202 }
1203 }
1204
1205 return false;
1206 }
1207
1208
1217 protected boolean matchesByMonthDay(Calendar candidate) {
1218 return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
1219 }
1220
1221
1230 protected boolean matchesByYearDay(Calendar candidate) {
1231 return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1232 }
1233
1234
1243 protected boolean matchesByWeekNo(Calendar candidate) {
1244 return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1245 }
1246
1247
1256 protected boolean matchesByMonth(Calendar candidate) {
1257 return matchesByField(byMonth, Calendar.MONTH, candidate, false);
1258 }
1259
1260
1267 public String toString() {
1268 StringBuilder sb = new StringBuilder();
1269
1270 sb.append(getClass().getName());
1271 sb.append("[dtStart=");
1272 sb.append((dtStart != null) ? dtStart.toString() : "null");
1273 sb.append(",duration=");
1274 sb.append((duration != null) ? duration.toString() : "null");
1275 sb.append(",frequency=");
1276 sb.append(frequency);
1277 sb.append(",interval=");
1278 sb.append(interval);
1279 sb.append(",until=");
1280 sb.append((until != null) ? until.toString() : "null");
1281 sb.append(",byDay=");
1282
1283 if (byDay == null) {
1284 sb.append("null");
1285 }
1286 else {
1287 sb.append("[");
1288
1289 for (int i = 0; i < byDay.length; i++) {
1290 if (i != 0) {
1291 sb.append(",");
1292 }
1293
1294 if (byDay[i] != null) {
1295 sb.append(byDay[i].toString());
1296 }
1297 else {
1298 sb.append("null");
1299 }
1300 }
1301
1302 sb.append("]");
1303 }
1304
1305 sb.append(",byMonthDay=");
1306 sb.append(stringizeIntArray(byMonthDay));
1307 sb.append(",byYearDay=");
1308 sb.append(stringizeIntArray(byYearDay));
1309 sb.append(",byWeekNo=");
1310 sb.append(stringizeIntArray(byWeekNo));
1311 sb.append(",byMonth=");
1312 sb.append(stringizeIntArray(byMonth));
1313 sb.append(']');
1314
1315 return sb.toString();
1316 }
1317
1318
1327 private String stringizeIntArray(int[] a) {
1328 if (a == null) {
1329 return "null";
1330 }
1331
1332 StringBuilder sb = new StringBuilder();
1333
1334 sb.append("[");
1335
1336 for (int i = 0; i < a.length; i++) {
1337 if (i != 0) {
1338 sb.append(",");
1339 }
1340
1341 sb.append(a[i]);
1342 }
1343
1344 sb.append("]");
1345
1346 return sb.toString();
1347 }
1348
1349}