1
19
20
49
50 package com.liferay.util.cal;
51
52 import com.liferay.portal.kernel.util.CalendarFactoryUtil;
53 import com.liferay.portal.kernel.util.StringPool;
54
55 import java.io.Serializable;
56
57 import java.util.Calendar;
58 import java.util.Date;
59 import java.util.TimeZone;
60
61
70 public class Recurrence implements Serializable {
71
72
75 public final static int DAILY = 3;
76
77
80 public final static int WEEKLY = 4;
81
82
85 public final static int MONTHLY = 5;
86
87
90 public final static int YEARLY = 6;
91
92
95 public final static int NO_RECURRENCE = 7;
96
97
100 protected Calendar dtStart;
101
102
105 protected Duration duration;
106
107
110 protected int frequency;
111
112
115 protected int interval;
116
117
120 protected int occurrence = 0;
121
122
125 protected Calendar until;
126
127
130 protected DayAndPosition[] byDay;
131
132
135 protected int[] byMonthDay;
136
137
140 protected int[] byYearDay;
141
142
145 protected int[] byWeekNo;
146
147
150 protected int[] byMonth;
151
152
157 public Recurrence() {
158 this(null, new Duration(), NO_RECURRENCE);
159 }
160
161
169 public Recurrence(Calendar start, Duration dur) {
170 this(start, dur, NO_RECURRENCE);
171 }
172
173
182 public Recurrence(Calendar start, Duration dur, int freq) {
183 setDtStart(start);
184
185 duration = (Duration)dur.clone();
186 frequency = freq;
187 interval = 1;
188 }
189
190
191
192
199 public Calendar getDtStart() {
200 return (Calendar)dtStart.clone();
201 }
202
203
210 public void setDtStart(Calendar start) {
211 int oldStart;
212
213 if (dtStart != null) {
214 oldStart = dtStart.getFirstDayOfWeek();
215 }
216 else {
217 oldStart = Calendar.MONDAY;
218 }
219
220 if (start == null) {
221 dtStart = CalendarFactoryUtil.getCalendar(
222 TimeZone.getTimeZone(StringPool.UTC));
223
224 dtStart.setTime(new Date(0L));
225 }
226 else {
227 dtStart = (Calendar)start.clone();
228
229 dtStart.clear(Calendar.ZONE_OFFSET);
230 dtStart.clear(Calendar.DST_OFFSET);
231 dtStart.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
232 }
233
234 dtStart.setMinimalDaysInFirstWeek(4);
235 dtStart.setFirstDayOfWeek(oldStart);
236 }
237
238
245 public Duration getDuration() {
246 return (Duration)duration.clone();
247 }
248
249
256 public void setDuration(Duration d) {
257 duration = (Duration)d.clone();
258 }
259
260
267 public Calendar getDtEnd() {
268
269
273 Calendar tempEnd = (Calendar)dtStart.clone();
274
275 tempEnd.setTime(new Date(dtStart.getTime().getTime()
276 + duration.getInterval()));
277
278 return tempEnd;
279 }
280
281
288 public void setDtEnd(Calendar end) {
289 Calendar tempEnd = (Calendar)end.clone();
290
291 tempEnd.clear(Calendar.ZONE_OFFSET);
292 tempEnd.clear(Calendar.DST_OFFSET);
293 tempEnd.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
294 duration.setInterval(tempEnd.getTime().getTime()
295 - dtStart.getTime().getTime());
296 }
297
298
305 public int getFrequency() {
306 return frequency;
307 }
308
309
316 public void setFrequency(int freq) {
317 if ((frequency != DAILY) && (frequency != WEEKLY)
318 && (frequency != MONTHLY) && (frequency != YEARLY)
319 && (frequency != NO_RECURRENCE)) {
320 throw new IllegalArgumentException("Invalid frequency");
321 }
322
323 frequency = freq;
324 }
325
326
333 public int getInterval() {
334 return interval;
335 }
336
337
344 public void setInterval(int intr) {
345 interval = (intr > 0) ? intr : 1;
346 }
347
348
355 public int getOccurrence() {
356 return occurrence;
357 }
358
359
366 public void setOccurrence(int occur) {
367 occurrence = occur;
368 }
369
370
377 public Calendar getUntil() {
378 return ((until != null) ? (Calendar)until.clone() : null);
379 }
380
381
388 public void setUntil(Calendar u) {
389 if (u == null) {
390 until = null;
391
392 return;
393 }
394
395 until = (Calendar)u.clone();
396
397 until.clear(Calendar.ZONE_OFFSET);
398 until.clear(Calendar.DST_OFFSET);
399 until.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
400 }
401
402
409 public int getWeekStart() {
410 return dtStart.getFirstDayOfWeek();
411 }
412
413
420 public void setWeekStart(int weekstart) {
421 dtStart.setFirstDayOfWeek(weekstart);
422 }
423
424
431 public DayAndPosition[] getByDay() {
432 if (byDay == null) {
433 return null;
434 }
435
436 DayAndPosition[] b = new DayAndPosition[byDay.length];
437
438
442 for (int i = 0; i < byDay.length; i++) {
443 b[i] = (DayAndPosition)byDay[i].clone();
444 }
445
446 return b;
447 }
448
449
456 public void setByDay(DayAndPosition[] b) {
457 if (b == null) {
458 byDay = null;
459
460 return;
461 }
462
463 byDay = new DayAndPosition[b.length];
464
465
469 for (int i = 0; i < b.length; i++) {
470 byDay[i] = (DayAndPosition)b[i].clone();
471 }
472 }
473
474
481 public int[] getByMonthDay() {
482 if (byMonthDay == null) {
483 return null;
484 }
485
486 int[] b = new int[byMonthDay.length];
487
488 System.arraycopy(byMonthDay, 0, b, 0, byMonthDay.length);
489
490 return b;
491 }
492
493
500 public void setByMonthDay(int[] b) {
501 if (b == null) {
502 byMonthDay = null;
503
504 return;
505 }
506
507 byMonthDay = new int[b.length];
508
509 System.arraycopy(b, 0, byMonthDay, 0, b.length);
510 }
511
512
519 public int[] getByYearDay() {
520 if (byYearDay == null) {
521 return null;
522 }
523
524 int[] b = new int[byYearDay.length];
525
526 System.arraycopy(byYearDay, 0, b, 0, byYearDay.length);
527
528 return b;
529 }
530
531
538 public void setByYearDay(int[] b) {
539 if (b == null) {
540 byYearDay = null;
541
542 return;
543 }
544
545 byYearDay = new int[b.length];
546
547 System.arraycopy(b, 0, byYearDay, 0, b.length);
548 }
549
550
557 public int[] getByWeekNo() {
558 if (byWeekNo == null) {
559 return null;
560 }
561
562 int[] b = new int[byWeekNo.length];
563
564 System.arraycopy(byWeekNo, 0, b, 0, byWeekNo.length);
565
566 return b;
567 }
568
569
576 public void setByWeekNo(int[] b) {
577 if (b == null) {
578 byWeekNo = null;
579
580 return;
581 }
582
583 byWeekNo = new int[b.length];
584
585 System.arraycopy(b, 0, byWeekNo, 0, b.length);
586 }
587
588
595 public int[] getByMonth() {
596 if (byMonth == null) {
597 return null;
598 }
599
600 int[] b = new int[byMonth.length];
601
602 System.arraycopy(byMonth, 0, b, 0, byMonth.length);
603
604 return b;
605 }
606
607
614 public void setByMonth(int[] b) {
615 if (b == null) {
616 byMonth = null;
617
618 return;
619 }
620
621 byMonth = new int[b.length];
622
623 System.arraycopy(b, 0, byMonth, 0, b.length);
624 }
625
626
635 public boolean isInRecurrence(Calendar current) {
636 return isInRecurrence(current, false);
637 }
638
639
649 public boolean isInRecurrence(Calendar current, boolean debug) {
650 Calendar myCurrent = (Calendar)current.clone();
651
652
654 myCurrent.clear(Calendar.ZONE_OFFSET);
655 myCurrent.clear(Calendar.DST_OFFSET);
656 myCurrent.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
657 myCurrent.setMinimalDaysInFirstWeek(4);
658 myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
659
660 if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
661
662
664 if (debug) {
665 System.err.println("current < start");
666 }
667
668 return false;
669 }
670
671 if (myCurrent.getTime().getTime()
672 < dtStart.getTime().getTime() + duration.getInterval()) {
673
674
676 if (debug) {
677 System.err.println("within duration of start");
678 }
679
680 return true;
681 }
682
683 Calendar candidate = getCandidateStartTime(myCurrent);
684
685
686
687 while (candidate.getTime().getTime() + duration.getInterval()
688 > myCurrent.getTime().getTime()) {
689 if (candidateIsInRecurrence(candidate, debug)) {
690 return true;
691 }
692
693
694
695 candidate.add(Calendar.SECOND, -1);
696
697
698
699 if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
700 if (debug) {
701 System.err.println("No candidates after dtStart");
702 }
703
704 return false;
705 }
706
707 candidate = getCandidateStartTime(candidate);
708 }
709
710 if (debug) {
711 System.err.println("No matching candidates");
712 }
713
714 return false;
715 }
716
717
727 protected boolean candidateIsInRecurrence(Calendar candidate,
728 boolean debug) {
729 if ((until != null)
730 && (candidate.getTime().getTime() > until.getTime().getTime())) {
731
732
734 if (debug) {
735 System.err.println("after until");
736 }
737
738 return false;
739 }
740
741 if (getRecurrenceCount(candidate) % interval != 0) {
742
743
745 if (debug) {
746 System.err.println("not an interval rep");
747 }
748
749 return false;
750 }
751 else if ((occurrence > 0) &&
752 (getRecurrenceCount(candidate) >= occurrence)) {
753
754 return false;
755 }
756
757 if (!matchesByDay(candidate) ||!matchesByMonthDay(candidate)
758 ||!matchesByYearDay(candidate) ||!matchesByWeekNo(candidate)
759 ||!matchesByMonth(candidate)) {
760
761
763 if (debug) {
764 System.err.println("doesn't match a by*");
765 }
766
767 return false;
768 }
769
770 if (debug) {
771 System.err.println("All checks succeeded");
772 }
773
774 return true;
775 }
776
777
784 protected int getMinimumInterval() {
785 if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null)
786 || (byYearDay != null)) {
787 return DAILY;
788 }
789 else if ((frequency == WEEKLY) || (byWeekNo != null)) {
790 return WEEKLY;
791 }
792 else if ((frequency == MONTHLY) || (byMonth != null)) {
793 return MONTHLY;
794 }
795 else if (frequency == YEARLY) {
796 return YEARLY;
797 }
798 else if (frequency == NO_RECURRENCE) {
799 return NO_RECURRENCE;
800 }
801 else {
802
803
805 throw new IllegalStateException(
806 "Internal error: Unknown frequency value");
807 }
808 }
809
810
819 public Calendar getCandidateStartTime(Calendar current) {
820 if (dtStart.getTime().getTime() > current.getTime().getTime()) {
821 throw new IllegalArgumentException("Current time before DtStart");
822 }
823
824 int minInterval = getMinimumInterval();
825 Calendar candidate = (Calendar)current.clone();
826
827 if (true) {
828
829
831 candidate.clear(Calendar.ZONE_OFFSET);
832 candidate.clear(Calendar.DST_OFFSET);
833 candidate.setTimeZone(TimeZone.getTimeZone(StringPool.UTC));
834 candidate.setMinimalDaysInFirstWeek(4);
835 candidate.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
836 }
837
838 if (frequency == NO_RECURRENCE) {
839 candidate.setTime(dtStart.getTime());
840
841 return candidate;
842 }
843
844 reduce_constant_length_field(Calendar.SECOND, dtStart, candidate);
845 reduce_constant_length_field(Calendar.MINUTE, dtStart, candidate);
846 reduce_constant_length_field(Calendar.HOUR_OF_DAY, dtStart, candidate);
847
848 switch (minInterval) {
849
850 case DAILY :
851
852
853
854 break;
855
856 case WEEKLY :
857 reduce_constant_length_field(Calendar.DAY_OF_WEEK, dtStart,
858 candidate);
859 break;
860
861 case MONTHLY :
862 reduce_day_of_month(dtStart, candidate);
863 break;
864
865 case YEARLY :
866 reduce_day_of_year(dtStart, candidate);
867 break;
868 }
869
870 return candidate;
871 }
872
873
882 protected static void reduce_constant_length_field(int field,
883 Calendar start,
884 Calendar candidate) {
885 if ((start.getMaximum(field) != start.getLeastMaximum(field))
886 || (start.getMinimum(field) != start.getGreatestMinimum(field))) {
887 throw new IllegalArgumentException("Not a constant length field");
888 }
889
890 int fieldLength = (start.getMaximum(field) - start.getMinimum(field)
891 + 1);
892 int delta = start.get(field) - candidate.get(field);
893
894 if (delta > 0) {
895 delta -= fieldLength;
896 }
897
898 candidate.add(field, delta);
899 }
900
901
909 protected static void reduce_day_of_month(Calendar start,
910 Calendar candidate) {
911 Calendar tempCal = (Calendar)candidate.clone();
912
913 tempCal.add(Calendar.MONTH, -1);
914
915 int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
916
917 if (delta > 0) {
918 delta -= tempCal.getActualMaximum(Calendar.DATE);
919 }
920
921 candidate.add(Calendar.DATE, delta);
922
923 while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
924 tempCal.add(Calendar.MONTH, -1);
925 candidate.add(Calendar.DATE,
926 -tempCal.getActualMaximum(Calendar.DATE));
927 }
928 }
929
930
938 protected static void reduce_day_of_year(Calendar start,
939 Calendar candidate) {
940 if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH))
941 || ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH))
942 && (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
943 candidate.add(Calendar.YEAR, -1);
944 }
945
946
947
948 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
949 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
950
951 while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH))
952 || (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
953 candidate.add(Calendar.YEAR, -1);
954 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
955 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
956 }
957 }
958
959
968 protected int getRecurrenceCount(Calendar candidate) {
969 switch (frequency) {
970
971 case NO_RECURRENCE :
972 return 0;
973
974 case DAILY :
975 return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
976
977 case WEEKLY :
978 Calendar tempCand = (Calendar)candidate.clone();
979
980 tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
981
982 return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
983
984 case MONTHLY :
985 return (int)(getMonthNumber(candidate)
986 - getMonthNumber(dtStart));
987
988 case YEARLY :
989 return candidate.get(Calendar.YEAR)
990 - dtStart.get(Calendar.YEAR);
991
992 default :
993 throw new IllegalStateException("bad frequency internally...");
994 }
995 }
996
997
1006 protected static long getDayNumber(Calendar cal) {
1007 Calendar tempCal = (Calendar)cal.clone();
1008
1009
1011 tempCal.set(Calendar.MILLISECOND, 0);
1012 tempCal.set(Calendar.SECOND, 0);
1013 tempCal.set(Calendar.MINUTE, 0);
1014 tempCal.set(Calendar.HOUR_OF_DAY, 0);
1015
1016 return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
1017 }
1018
1019
1028 protected static long getWeekNumber(Calendar cal) {
1029 Calendar tempCal = (Calendar)cal.clone();
1030
1031
1033 tempCal.set(Calendar.MILLISECOND, 0);
1034 tempCal.set(Calendar.SECOND, 0);
1035 tempCal.set(Calendar.MINUTE, 0);
1036 tempCal.set(Calendar.HOUR_OF_DAY, 0);
1037
1038
1040 int delta = tempCal.getFirstDayOfWeek()
1041 - tempCal.get(Calendar.DAY_OF_WEEK);
1042
1043 if (delta > 0) {
1044 delta -= 7;
1045 }
1046
1047
1049
1052 long weekEpoch = (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24
1053 * 60 * 60 * 1000L;
1054
1055 return (tempCal.getTime().getTime() - weekEpoch)
1056 / (7 * 24 * 60 * 60 * 1000);
1057 }
1058
1059
1068 protected static long getMonthNumber(Calendar cal) {
1069 return (cal.get(Calendar.YEAR) - 1970) * 12
1070 + (cal.get(Calendar.MONTH) - Calendar.JANUARY);
1071 }
1072
1073
1082 protected boolean matchesByDay(Calendar candidate) {
1083 if ((byDay == null) || (byDay.length == 0)) {
1084
1085
1086
1087 return true;
1088 }
1089
1090 int i;
1091
1092 for (i = 0; i < byDay.length; i++) {
1093 if (matchesIndividualByDay(candidate, byDay[i])) {
1094 return true;
1095 }
1096 }
1097
1098 return false;
1099 }
1100
1101
1111 protected boolean matchesIndividualByDay(Calendar candidate,
1112 DayAndPosition pos) {
1113 if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
1114 return false;
1115 }
1116
1117 int position = pos.getDayPosition();
1118
1119 if (position == 0) {
1120 return true;
1121 }
1122
1123 int field;
1124
1125 switch (frequency) {
1126
1127 case MONTHLY :
1128 field = Calendar.DAY_OF_MONTH;
1129 break;
1130
1131 case YEARLY :
1132 field = Calendar.DAY_OF_YEAR;
1133 break;
1134
1135 default :
1136 throw new IllegalStateException(
1137 "byday has a day position "
1138 + "in non-MONTHLY or YEARLY recurrence");
1139 }
1140
1141 if (position > 0) {
1142 int day_of_week_in_field = ((candidate.get(field) - 1) / 7) + 1;
1143
1144 return (position == day_of_week_in_field);
1145 }
1146 else {
1147
1148
1149
1150 int negative_day_of_week_in_field =
1151 ((candidate.getActualMaximum(field) - candidate.get(field)) / 7)
1152 + 1;
1153
1154 return (-position == negative_day_of_week_in_field);
1155 }
1156 }
1157
1158
1170 protected static boolean matchesByField(int[] array, int field,
1171 Calendar candidate,
1172 boolean allowNegative) {
1173 if ((array == null) || (array.length == 0)) {
1174
1175
1176
1177 return true;
1178 }
1179
1180 int i;
1181
1182 for (i = 0; i < array.length; i++) {
1183 int val;
1184
1185 if (allowNegative && (array[i] < 0)) {
1186
1187
1189 int max = candidate.getActualMaximum(field);
1190
1191 val = (max + 1) + array[i];
1192 }
1193 else {
1194 val = array[i];
1195 }
1196
1197 if (val == candidate.get(field)) {
1198 return true;
1199 }
1200 }
1201
1202 return false;
1203 }
1204
1205
1214 protected boolean matchesByMonthDay(Calendar candidate) {
1215 return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
1216 }
1217
1218
1227 protected boolean matchesByYearDay(Calendar candidate) {
1228 return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1229 }
1230
1231
1240 protected boolean matchesByWeekNo(Calendar candidate) {
1241 return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1242 }
1243
1244
1253 protected boolean matchesByMonth(Calendar candidate) {
1254 return matchesByField(byMonth, Calendar.MONTH, candidate, false);
1255 }
1256
1257
1264 public String toString() {
1265 StringBuilder sb = new StringBuilder();
1266
1267 sb.append(getClass().getName());
1268 sb.append("[dtStart=");
1269 sb.append((dtStart != null) ? dtStart.toString() : "null");
1270 sb.append(",duration=");
1271 sb.append((duration != null) ? duration.toString() : "null");
1272 sb.append(",frequency=");
1273 sb.append(frequency);
1274 sb.append(",interval=");
1275 sb.append(interval);
1276 sb.append(",until=");
1277 sb.append((until != null) ? until.toString() : "null");
1278 sb.append(",byDay=");
1279
1280 if (byDay == null) {
1281 sb.append("null");
1282 }
1283 else {
1284 sb.append("[");
1285
1286 for (int i = 0; i < byDay.length; i++) {
1287 if (i != 0) {
1288 sb.append(",");
1289 }
1290
1291 if (byDay[i] != null) {
1292 sb.append(byDay[i].toString());
1293 }
1294 else {
1295 sb.append("null");
1296 }
1297 }
1298
1299 sb.append("]");
1300 }
1301
1302 sb.append(",byMonthDay=");
1303 sb.append(stringizeIntArray(byMonthDay));
1304 sb.append(",byYearDay=");
1305 sb.append(stringizeIntArray(byYearDay));
1306 sb.append(",byWeekNo=");
1307 sb.append(stringizeIntArray(byWeekNo));
1308 sb.append(",byMonth=");
1309 sb.append(stringizeIntArray(byMonth));
1310 sb.append(']');
1311
1312 return sb.toString();
1313 }
1314
1315
1324 private String stringizeIntArray(int[] a) {
1325 if (a == null) {
1326 return "null";
1327 }
1328
1329 StringBuilder sb = new StringBuilder();
1330
1331 sb.append("[");
1332
1333 for (int i = 0; i < a.length; i++) {
1334 if (i != 0) {
1335 sb.append(",");
1336 }
1337
1338 sb.append(a[i]);
1339 }
1340
1341 sb.append("]");
1342
1343 return sb.toString();
1344 }
1345
1346}