1
30
31 package com.liferay.util.cal;
32
33 import com.liferay.portal.kernel.util.CalendarFactoryUtil;
34 import com.liferay.portal.kernel.util.StringMaker;
35
36 import java.io.Serializable;
37
38 import java.util.Calendar;
39 import java.util.Date;
40 import java.util.TimeZone;
41
42
51 public class Recurrence implements Serializable {
52
53
56 public final static int DAILY = 3;
57
58
61 public final static int WEEKLY = 4;
62
63
66 public final static int MONTHLY = 5;
67
68
71 public final static int YEARLY = 6;
72
73
76 public final static int NO_RECURRENCE = 7;
77
78
81 protected Calendar dtStart;
82
83
86 protected Duration duration;
87
88
91 protected int frequency;
92
93
96 protected int interval;
97
98
101 protected int occurrence = 0;
102
103
106 protected Calendar until;
107
108
111 protected DayAndPosition[] byDay;
112
113
116 protected int[] byMonthDay;
117
118
121 protected int[] byYearDay;
122
123
126 protected int[] byWeekNo;
127
128
131 protected int[] byMonth;
132
133
138 public Recurrence() {
139 this(null, new Duration(), NO_RECURRENCE);
140 }
141
142
150 public Recurrence(Calendar start, Duration dur) {
151 this(start, dur, NO_RECURRENCE);
152 }
153
154
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
180 public Calendar getDtStart() {
181 return (Calendar)dtStart.clone();
182 }
183
184
191 public void setDtStart(Calendar start) {
192 int oldStart;
193
194 if (dtStart != null) {
195 oldStart = dtStart.getFirstDayOfWeek();
196 }
197 else {
198 oldStart = Calendar.MONDAY;
199 }
200
201 if (start == null) {
202 dtStart = CalendarFactoryUtil.getCalendar(
203 TimeZone.getTimeZone("GMT"));
204
205 dtStart.setTime(new Date(0L));
206 }
207 else {
208 dtStart = (Calendar)start.clone();
209
210 dtStart.clear(Calendar.ZONE_OFFSET);
211 dtStart.clear(Calendar.DST_OFFSET);
212 dtStart.setTimeZone(TimeZone.getTimeZone("GMT"));
213 }
214
215 dtStart.setMinimalDaysInFirstWeek(4);
216 dtStart.setFirstDayOfWeek(oldStart);
217 }
218
219
226 public Duration getDuration() {
227 return (Duration)duration.clone();
228 }
229
230
237 public void setDuration(Duration d) {
238 duration = (Duration)d.clone();
239 }
240
241
248 public Calendar getDtEnd() {
249
250
254 Calendar tempEnd = (Calendar)dtStart.clone();
255
256 tempEnd.setTime(new Date(dtStart.getTime().getTime()
257 + duration.getInterval()));
258
259 return tempEnd;
260 }
261
262
269 public void setDtEnd(Calendar end) {
270 Calendar tempEnd = (Calendar)end.clone();
271
272 tempEnd.clear(Calendar.ZONE_OFFSET);
273 tempEnd.clear(Calendar.DST_OFFSET);
274 tempEnd.setTimeZone(TimeZone.getTimeZone("GMT"));
275 duration.setInterval(tempEnd.getTime().getTime()
276 - dtStart.getTime().getTime());
277 }
278
279
286 public int getFrequency() {
287 return frequency;
288 }
289
290
297 public void setFrequency(int freq) {
298 if ((frequency != DAILY) && (frequency != WEEKLY)
299 && (frequency != MONTHLY) && (frequency != YEARLY)
300 && (frequency != NO_RECURRENCE)) {
301 throw new IllegalArgumentException("Invalid frequency");
302 }
303
304 frequency = freq;
305 }
306
307
314 public int getInterval() {
315 return interval;
316 }
317
318
325 public void setInterval(int intr) {
326 interval = intr;
327 }
328
329
336 public int getOccurrence() {
337 return occurrence;
338 }
339
340
347 public void setOccurrence(int occur) {
348 occurrence = occur;
349 }
350
351
358 public Calendar getUntil() {
359 return ((until != null) ? (Calendar)until.clone() : null);
360 }
361
362
369 public void setUntil(Calendar u) {
370 if (u == null) {
371 until = null;
372
373 return;
374 }
375
376 until = (Calendar)u.clone();
377
378 until.clear(Calendar.ZONE_OFFSET);
379 until.clear(Calendar.DST_OFFSET);
380 until.setTimeZone(TimeZone.getTimeZone("GMT"));
381 }
382
383
390 public int getWeekStart() {
391 return dtStart.getFirstDayOfWeek();
392 }
393
394
401 public void setWeekStart(int weekstart) {
402 dtStart.setFirstDayOfWeek(weekstart);
403 }
404
405
412 public DayAndPosition[] getByDay() {
413 if (byDay == null) {
414 return null;
415 }
416
417 DayAndPosition[] b = new DayAndPosition[byDay.length];
418
419
423 for (int i = 0; i < byDay.length; i++) {
424 b[i] = (DayAndPosition)byDay[i].clone();
425 }
426
427 return b;
428 }
429
430
437 public void setByDay(DayAndPosition[] b) {
438 if (b == null) {
439 byDay = null;
440
441 return;
442 }
443
444 byDay = new DayAndPosition[b.length];
445
446
450 for (int i = 0; i < b.length; i++) {
451 byDay[i] = (DayAndPosition)b[i].clone();
452 }
453 }
454
455
462 public int[] getByMonthDay() {
463 if (byMonthDay == null) {
464 return null;
465 }
466
467 int[] b = new int[byMonthDay.length];
468
469 System.arraycopy(byMonthDay, 0, b, 0, byMonthDay.length);
470
471 return b;
472 }
473
474
481 public void setByMonthDay(int[] b) {
482 if (b == null) {
483 byMonthDay = null;
484
485 return;
486 }
487
488 byMonthDay = new int[b.length];
489
490 System.arraycopy(b, 0, byMonthDay, 0, b.length);
491 }
492
493
500 public int[] getByYearDay() {
501 if (byYearDay == null) {
502 return null;
503 }
504
505 int[] b = new int[byYearDay.length];
506
507 System.arraycopy(byYearDay, 0, b, 0, byYearDay.length);
508
509 return b;
510 }
511
512
519 public void setByYearDay(int[] b) {
520 if (b == null) {
521 byYearDay = null;
522
523 return;
524 }
525
526 byYearDay = new int[b.length];
527
528 System.arraycopy(b, 0, byYearDay, 0, b.length);
529 }
530
531
538 public int[] getByWeekNo() {
539 if (byWeekNo == null) {
540 return null;
541 }
542
543 int[] b = new int[byWeekNo.length];
544
545 System.arraycopy(byWeekNo, 0, b, 0, byWeekNo.length);
546
547 return b;
548 }
549
550
557 public void setByWeekNo(int[] b) {
558 if (b == null) {
559 byWeekNo = null;
560
561 return;
562 }
563
564 byWeekNo = new int[b.length];
565
566 System.arraycopy(b, 0, byWeekNo, 0, b.length);
567 }
568
569
576 public int[] getByMonth() {
577 if (byMonth == null) {
578 return null;
579 }
580
581 int[] b = new int[byMonth.length];
582
583 System.arraycopy(byMonth, 0, b, 0, byMonth.length);
584
585 return b;
586 }
587
588
595 public void setByMonth(int[] b) {
596 if (b == null) {
597 byMonth = null;
598
599 return;
600 }
601
602 byMonth = new int[b.length];
603
604 System.arraycopy(b, 0, byMonth, 0, b.length);
605 }
606
607
616 public boolean isInRecurrence(Calendar current) {
617 return isInRecurrence(current, false);
618 }
619
620
630 public boolean isInRecurrence(Calendar current, boolean debug) {
631 Calendar myCurrent = (Calendar)current.clone();
632
633
635 myCurrent.clear(Calendar.ZONE_OFFSET);
636 myCurrent.clear(Calendar.DST_OFFSET);
637 myCurrent.setTimeZone(TimeZone.getTimeZone("GMT"));
638 myCurrent.setMinimalDaysInFirstWeek(4);
639 myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
640
641 if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
642
643
645 if (debug) {
646 System.err.println("current < start");
647 }
648
649 return false;
650 }
651
652 if (myCurrent.getTime().getTime()
653 < dtStart.getTime().getTime() + duration.getInterval()) {
654
655
657 if (debug) {
658 System.err.println("within duration of start");
659 }
660
661 return true;
662 }
663
664 Calendar candidate = getCandidateStartTime(myCurrent);
665
666
667
668 while (candidate.getTime().getTime() + duration.getInterval()
669 > myCurrent.getTime().getTime()) {
670 if (candidateIsInRecurrence(candidate, debug)) {
671 return true;
672 }
673
674
675
676 candidate.add(Calendar.SECOND, -1);
677
678
679
680 if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
681 if (debug) {
682 System.err.println("No candidates after dtStart");
683 }
684
685 return false;
686 }
687
688 candidate = getCandidateStartTime(candidate);
689 }
690
691 if (debug) {
692 System.err.println("No matching candidates");
693 }
694
695 return false;
696 }
697
698
708 protected boolean candidateIsInRecurrence(Calendar candidate,
709 boolean debug) {
710 if ((until != null)
711 && (candidate.getTime().getTime() > until.getTime().getTime())) {
712
713
715 if (debug) {
716 System.err.println("after until");
717 }
718
719 return false;
720 }
721
722 if (getRecurrenceCount(candidate) % interval != 0) {
723
724
726 if (debug) {
727 System.err.println("not an interval rep");
728 }
729
730 return false;
731 }
732 else if ((occurrence > 0) &&
733 (getRecurrenceCount(candidate) >= occurrence)) {
734
735 return false;
736 }
737
738 if (!matchesByDay(candidate) ||!matchesByMonthDay(candidate)
739 ||!matchesByYearDay(candidate) ||!matchesByWeekNo(candidate)
740 ||!matchesByMonth(candidate)) {
741
742
744 if (debug) {
745 System.err.println("doesn't match a by*");
746 }
747
748 return false;
749 }
750
751 if (debug) {
752 System.err.println("All checks succeeded");
753 }
754
755 return true;
756 }
757
758
765 protected int getMinimumInterval() {
766 if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null)
767 || (byYearDay != null)) {
768 return DAILY;
769 }
770 else if ((frequency == WEEKLY) || (byWeekNo != null)) {
771 return WEEKLY;
772 }
773 else if ((frequency == MONTHLY) || (byMonth != null)) {
774 return MONTHLY;
775 }
776 else if (frequency == YEARLY) {
777 return YEARLY;
778 }
779 else if (frequency == NO_RECURRENCE) {
780 return NO_RECURRENCE;
781 }
782 else {
783
784
786 throw new IllegalStateException(
787 "Internal error: Unknown frequency value");
788 }
789 }
790
791
800 public Calendar getCandidateStartTime(Calendar current) {
801 if (dtStart.getTime().getTime() > current.getTime().getTime()) {
802 throw new IllegalArgumentException("Current time before DtStart");
803 }
804
805 int minInterval = getMinimumInterval();
806 Calendar candidate = (Calendar)current.clone();
807
808 if (true) {
809
810
812 candidate.clear(Calendar.ZONE_OFFSET);
813 candidate.clear(Calendar.DST_OFFSET);
814 candidate.setTimeZone(TimeZone.getTimeZone("GMT"));
815 candidate.setMinimalDaysInFirstWeek(4);
816 candidate.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
817 }
818
819 if (frequency == NO_RECURRENCE) {
820 candidate.setTime(dtStart.getTime());
821
822 return candidate;
823 }
824
825 reduce_constant_length_field(Calendar.SECOND, dtStart, candidate);
826 reduce_constant_length_field(Calendar.MINUTE, dtStart, candidate);
827 reduce_constant_length_field(Calendar.HOUR_OF_DAY, dtStart, candidate);
828
829 switch (minInterval) {
830
831 case DAILY :
832
833
834
835 break;
836
837 case WEEKLY :
838 reduce_constant_length_field(Calendar.DAY_OF_WEEK, dtStart,
839 candidate);
840 break;
841
842 case MONTHLY :
843 reduce_day_of_month(dtStart, candidate);
844 break;
845
846 case YEARLY :
847 reduce_day_of_year(dtStart, candidate);
848 break;
849 }
850
851 return candidate;
852 }
853
854
863 protected static void reduce_constant_length_field(int field,
864 Calendar start,
865 Calendar candidate) {
866 if ((start.getMaximum(field) != start.getLeastMaximum(field))
867 || (start.getMinimum(field) != start.getGreatestMinimum(field))) {
868 throw new IllegalArgumentException("Not a constant length field");
869 }
870
871 int fieldLength = (start.getMaximum(field) - start.getMinimum(field)
872 + 1);
873 int delta = start.get(field) - candidate.get(field);
874
875 if (delta > 0) {
876 delta -= fieldLength;
877 }
878
879 candidate.add(field, delta);
880 }
881
882
890 protected static void reduce_day_of_month(Calendar start,
891 Calendar candidate) {
892 Calendar tempCal = (Calendar)candidate.clone();
893
894 tempCal.add(Calendar.MONTH, -1);
895
896 int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
897
898 if (delta > 0) {
899 delta -= tempCal.getActualMaximum(Calendar.DATE);
900 }
901
902 candidate.add(Calendar.DATE, delta);
903
904 while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
905 tempCal.add(Calendar.MONTH, -1);
906 candidate.add(Calendar.DATE,
907 -tempCal.getActualMaximum(Calendar.DATE));
908 }
909 }
910
911
919 protected static void reduce_day_of_year(Calendar start,
920 Calendar candidate) {
921 if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH))
922 || ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH))
923 && (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
924 candidate.add(Calendar.YEAR, -1);
925 }
926
927
928
929 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
930 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
931
932 while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH))
933 || (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
934 candidate.add(Calendar.YEAR, -1);
935 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
936 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
937 }
938 }
939
940
949 protected int getRecurrenceCount(Calendar candidate) {
950 switch (frequency) {
951
952 case NO_RECURRENCE :
953 return 0;
954
955 case DAILY :
956 return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
957
958 case WEEKLY :
959 Calendar tempCand = (Calendar)candidate.clone();
960
961 tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
962
963 return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
964
965 case MONTHLY :
966 return (int)(getMonthNumber(candidate)
967 - getMonthNumber(dtStart));
968
969 case YEARLY :
970 return candidate.get(Calendar.YEAR)
971 - dtStart.get(Calendar.YEAR);
972
973 default :
974 throw new IllegalStateException("bad frequency internally...");
975 }
976 }
977
978
987 protected static long getDayNumber(Calendar cal) {
988 Calendar tempCal = (Calendar)cal.clone();
989
990
992 tempCal.set(Calendar.MILLISECOND, 0);
993 tempCal.set(Calendar.SECOND, 0);
994 tempCal.set(Calendar.MINUTE, 0);
995 tempCal.set(Calendar.HOUR_OF_DAY, 0);
996
997 return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
998 }
999
1000
1009 protected static long getWeekNumber(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
1021 int delta = tempCal.getFirstDayOfWeek()
1022 - tempCal.get(Calendar.DAY_OF_WEEK);
1023
1024 if (delta > 0) {
1025 delta -= 7;
1026 }
1027
1028
1030
1033 long weekEpoch = (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24
1034 * 60 * 60 * 1000L;
1035
1036 return (tempCal.getTime().getTime() - weekEpoch)
1037 / (7 * 24 * 60 * 60 * 1000);
1038 }
1039
1040
1049 protected static long getMonthNumber(Calendar cal) {
1050 return (cal.get(Calendar.YEAR) - 1970) * 12
1051 + (cal.get(Calendar.MONTH) - Calendar.JANUARY);
1052 }
1053
1054
1063 protected boolean matchesByDay(Calendar candidate) {
1064 if ((byDay == null) || (byDay.length == 0)) {
1065
1066
1067
1068 return true;
1069 }
1070
1071 int i;
1072
1073 for (i = 0; i < byDay.length; i++) {
1074 if (matchesIndividualByDay(candidate, byDay[i])) {
1075 return true;
1076 }
1077 }
1078
1079 return false;
1080 }
1081
1082
1092 protected boolean matchesIndividualByDay(Calendar candidate,
1093 DayAndPosition pos) {
1094 if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
1095 return false;
1096 }
1097
1098 int position = pos.getDayPosition();
1099
1100 if (position == 0) {
1101 return true;
1102 }
1103
1104 int field;
1105
1106 switch (frequency) {
1107
1108 case MONTHLY :
1109 field = Calendar.DAY_OF_MONTH;
1110 break;
1111
1112 case YEARLY :
1113 field = Calendar.DAY_OF_YEAR;
1114 break;
1115
1116 default :
1117 throw new IllegalStateException(
1118 "byday has a day position "
1119 + "in non-MONTHLY or YEARLY recurrence");
1120 }
1121
1122 if (position > 0) {
1123 int day_of_week_in_field = ((candidate.get(field) - 1) / 7) + 1;
1124
1125 return (position == day_of_week_in_field);
1126 }
1127 else {
1128
1129
1130
1131 int negative_day_of_week_in_field =
1132 ((candidate.getActualMaximum(field) - candidate.get(field)) / 7)
1133 + 1;
1134
1135 return (-position == negative_day_of_week_in_field);
1136 }
1137 }
1138
1139
1151 protected static boolean matchesByField(int[] array, int field,
1152 Calendar candidate,
1153 boolean allowNegative) {
1154 if ((array == null) || (array.length == 0)) {
1155
1156
1157
1158 return true;
1159 }
1160
1161 int i;
1162
1163 for (i = 0; i < array.length; i++) {
1164 int val;
1165
1166 if (allowNegative && (array[i] < 0)) {
1167
1168
1170 int max = candidate.getActualMaximum(field);
1171
1172 val = (max + 1) + array[i];
1173 }
1174 else {
1175 val = array[i];
1176 }
1177
1178 if (val == candidate.get(field)) {
1179 return true;
1180 }
1181 }
1182
1183 return false;
1184 }
1185
1186
1195 protected boolean matchesByMonthDay(Calendar candidate) {
1196 return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
1197 }
1198
1199
1208 protected boolean matchesByYearDay(Calendar candidate) {
1209 return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1210 }
1211
1212
1221 protected boolean matchesByWeekNo(Calendar candidate) {
1222 return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1223 }
1224
1225
1234 protected boolean matchesByMonth(Calendar candidate) {
1235 return matchesByField(byMonth, Calendar.MONTH, candidate, false);
1236 }
1237
1238
1245 public String toString() {
1246 StringMaker sm = new StringMaker();
1247
1248 sm.append(getClass().getName());
1249 sm.append("[dtStart=");
1250 sm.append((dtStart != null) ? dtStart.toString() : "null");
1251 sm.append(",duration=");
1252 sm.append((duration != null) ? duration.toString() : "null");
1253 sm.append(",frequency=");
1254 sm.append(frequency);
1255 sm.append(",interval=");
1256 sm.append(interval);
1257 sm.append(",until=");
1258 sm.append((until != null) ? until.toString() : "null");
1259 sm.append(",byDay=");
1260
1261 if (byDay == null) {
1262 sm.append("null");
1263 }
1264 else {
1265 sm.append("[");
1266
1267 for (int i = 0; i < byDay.length; i++) {
1268 if (i != 0) {
1269 sm.append(",");
1270 }
1271
1272 if (byDay[i] != null) {
1273 sm.append(byDay[i].toString());
1274 }
1275 else {
1276 sm.append("null");
1277 }
1278 }
1279
1280 sm.append("]");
1281 }
1282
1283 sm.append(",byMonthDay=");
1284 sm.append(stringizeIntArray(byMonthDay));
1285 sm.append(",byYearDay=");
1286 sm.append(stringizeIntArray(byYearDay));
1287 sm.append(",byWeekNo=");
1288 sm.append(stringizeIntArray(byWeekNo));
1289 sm.append(",byMonth=");
1290 sm.append(stringizeIntArray(byMonth));
1291 sm.append(']');
1292
1293 return sm.toString();
1294 }
1295
1296
1305 private String stringizeIntArray(int[] a) {
1306 if (a == null) {
1307 return "null";
1308 }
1309
1310 StringMaker sm = new StringMaker();
1311
1312 sm.append("[");
1313
1314 for (int i = 0; i < a.length; i++) {
1315 if (i != 0) {
1316 sm.append(",");
1317 }
1318
1319 sm.append(a[i]);
1320 }
1321
1322 sm.append("]");
1323
1324 return sm.toString();
1325 }
1326
1327}