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