001
014
015
044
045 package com.liferay.portal.kernel.cal;
046
047 import com.liferay.portal.kernel.util.ArrayUtil;
048 import com.liferay.portal.kernel.util.CalendarFactoryUtil;
049 import com.liferay.portal.kernel.util.StringBundler;
050 import com.liferay.portal.kernel.util.StringPool;
051 import com.liferay.portal.kernel.util.TimeZoneUtil;
052
053 import java.io.Serializable;
054
055 import java.util.Calendar;
056 import java.util.Date;
057
058
061 public class Recurrence implements Serializable {
062
063
066 public static final int DAILY = 3;
067
068
071 public static final int MONTHLY = 5;
072
073
076 public static final int NO_RECURRENCE = 7;
077
078
081 public static final int WEEKLY = 4;
082
083
086 public static final int YEARLY = 6;
087
088
091 public Recurrence() {
092 this(null, new Duration(), NO_RECURRENCE);
093 }
094
095
098 public Recurrence(Calendar start, Duration dur) {
099 this(start, dur, NO_RECURRENCE);
100 }
101
102
105 public Recurrence(Calendar start, Duration dur, int freq) {
106 setDtStart(start);
107
108 duration = (Duration)dur.clone();
109 frequency = freq;
110 interval = 1;
111 }
112
113
114
115
120 public DayAndPosition[] getByDay() {
121 if (byDay == null) {
122 return null;
123 }
124
125 DayAndPosition[] b = new DayAndPosition[byDay.length];
126
127
131 for (int i = 0; i < byDay.length; i++) {
132 b[i] = (DayAndPosition)byDay[i].clone();
133 }
134
135 return b;
136 }
137
138
143 public int[] getByMonth() {
144 if (byMonth == null) {
145 return null;
146 }
147
148 int[] b = new int[byMonth.length];
149
150 System.arraycopy(byMonth, 0, b, 0, byMonth.length);
151
152 return b;
153 }
154
155
160 public int[] getByMonthDay() {
161 if (byMonthDay == null) {
162 return null;
163 }
164
165 int[] b = new int[byMonthDay.length];
166
167 System.arraycopy(byMonthDay, 0, b, 0, byMonthDay.length);
168
169 return b;
170 }
171
172
177 public int[] getByWeekNo() {
178 if (byWeekNo == null) {
179 return null;
180 }
181
182 int[] b = new int[byWeekNo.length];
183
184 System.arraycopy(byWeekNo, 0, b, 0, byWeekNo.length);
185
186 return b;
187 }
188
189
194 public int[] getByYearDay() {
195 if (byYearDay == null) {
196 return null;
197 }
198
199 int[] b = new int[byYearDay.length];
200
201 System.arraycopy(byYearDay, 0, b, 0, byYearDay.length);
202
203 return b;
204 }
205
206
212 public Calendar getCandidateStartTime(Calendar current) {
213 if (dtStart.getTime().getTime() > current.getTime().getTime()) {
214 throw new IllegalArgumentException("Current time before DtStart");
215 }
216
217 int minInterval = getMinimumInterval();
218 Calendar candidate = (Calendar)current.clone();
219
220 if (true) {
221
222
223
224 candidate.clear(Calendar.ZONE_OFFSET);
225 candidate.clear(Calendar.DST_OFFSET);
226 candidate.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
227 candidate.setMinimalDaysInFirstWeek(4);
228 candidate.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
229 }
230
231 if (frequency == NO_RECURRENCE) {
232 candidate.setTime(dtStart.getTime());
233
234 return candidate;
235 }
236
237 reduce_constant_length_field(Calendar.SECOND, dtStart, candidate);
238 reduce_constant_length_field(Calendar.MINUTE, dtStart, candidate);
239 reduce_constant_length_field(Calendar.HOUR_OF_DAY, dtStart, candidate);
240
241 switch (minInterval) {
242
243 case DAILY :
244
245
246
247 break;
248
249 case WEEKLY :
250 reduce_constant_length_field(
251 Calendar.DAY_OF_WEEK, dtStart, candidate);
252 break;
253
254 case MONTHLY :
255 reduce_day_of_month(dtStart, candidate);
256 break;
257
258 case YEARLY :
259 reduce_day_of_year(dtStart, candidate);
260 break;
261 }
262
263 return candidate;
264 }
265
266
271 public Calendar getDtEnd() {
272
273
277 Calendar tempEnd = (Calendar)dtStart.clone();
278
279 tempEnd.setTime(
280 new Date(dtStart.getTime().getTime() + duration.getInterval()));
281
282 return tempEnd;
283 }
284
285
290 public Calendar getDtStart() {
291 return (Calendar)dtStart.clone();
292 }
293
294
299 public Duration getDuration() {
300 return (Duration)duration.clone();
301 }
302
303
308 public int getFrequency() {
309 return frequency;
310 }
311
312
317 public int getInterval() {
318 return interval;
319 }
320
321
326 public int getOccurrence() {
327 return occurrence;
328 }
329
330
335 public Calendar getUntil() {
336 return ((until != null) ? (Calendar)until.clone() : null);
337 }
338
339
344 public int getWeekStart() {
345 return dtStart.getFirstDayOfWeek();
346 }
347
348
354 public boolean isInRecurrence(Calendar current) {
355 return isInRecurrence(current, false);
356 }
357
358
365 public boolean isInRecurrence(Calendar current, boolean debug) {
366 Calendar myCurrent = (Calendar)current.clone();
367
368
369
370 myCurrent.clear(Calendar.ZONE_OFFSET);
371 myCurrent.clear(Calendar.DST_OFFSET);
372 myCurrent.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
373 myCurrent.setMinimalDaysInFirstWeek(4);
374 myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
375 myCurrent.set(Calendar.SECOND, 0);
376 myCurrent.set(Calendar.MILLISECOND, 0);
377
378 if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
379
380
381
382 if (debug) {
383 System.err.println("current < start");
384 }
385
386 return false;
387 }
388
389 Calendar candidate = getCandidateStartTime(myCurrent);
390
391
392
393 while ((candidate.getTime().getTime() + duration.getInterval()) >
394 myCurrent.getTime().getTime()) {
395
396 if (candidateIsInRecurrence(candidate, debug)) {
397 return true;
398 }
399
400
401
402 candidate.add(Calendar.SECOND, -1);
403
404
405
406 if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
407 if (debug) {
408 System.err.println("No candidates after dtStart");
409 }
410
411 return false;
412 }
413
414 candidate = getCandidateStartTime(candidate);
415 }
416
417 if (debug) {
418 System.err.println("No matching candidates");
419 }
420
421 return false;
422 }
423
424
427 public void setByDay(DayAndPosition[] b) {
428 if (b == null) {
429 byDay = null;
430
431 return;
432 }
433
434 byDay = new DayAndPosition[b.length];
435
436
440 for (int i = 0; i < b.length; i++) {
441 byDay[i] = (DayAndPosition)b[i].clone();
442 }
443 }
444
445
448 public void setByMonth(int[] b) {
449 if (b == null) {
450 byMonth = null;
451
452 return;
453 }
454
455 byMonth = new int[b.length];
456
457 System.arraycopy(b, 0, byMonth, 0, b.length);
458 }
459
460
463 public void setByMonthDay(int[] b) {
464 if (b == null) {
465 byMonthDay = null;
466
467 return;
468 }
469
470 byMonthDay = new int[b.length];
471
472 System.arraycopy(b, 0, byMonthDay, 0, b.length);
473 }
474
475
478 public void setByWeekNo(int[] b) {
479 if (b == null) {
480 byWeekNo = null;
481
482 return;
483 }
484
485 byWeekNo = new int[b.length];
486
487 System.arraycopy(b, 0, byWeekNo, 0, b.length);
488 }
489
490
493 public void setByYearDay(int[] b) {
494 if (b == null) {
495 byYearDay = null;
496
497 return;
498 }
499
500 byYearDay = new int[b.length];
501
502 System.arraycopy(b, 0, byYearDay, 0, b.length);
503 }
504
505
508 public void setDtEnd(Calendar end) {
509 Calendar tempEnd = (Calendar)end.clone();
510
511 tempEnd.clear(Calendar.ZONE_OFFSET);
512 tempEnd.clear(Calendar.DST_OFFSET);
513 tempEnd.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
514 duration.setInterval(
515 tempEnd.getTime().getTime() - dtStart.getTime().getTime());
516 }
517
518
521 public void setDtStart(Calendar start) {
522 int oldStart = 0;
523
524 if (dtStart != null) {
525 oldStart = dtStart.getFirstDayOfWeek();
526 }
527 else {
528 oldStart = Calendar.MONDAY;
529 }
530
531 if (start == null) {
532 dtStart = CalendarFactoryUtil.getCalendar(
533 TimeZoneUtil.getTimeZone(StringPool.UTC));
534
535 dtStart.setTime(new Date(0L));
536 }
537 else {
538 dtStart = (Calendar)start.clone();
539
540 dtStart.clear(Calendar.ZONE_OFFSET);
541 dtStart.clear(Calendar.DST_OFFSET);
542 dtStart.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
543 }
544
545 dtStart.setMinimalDaysInFirstWeek(4);
546 dtStart.setFirstDayOfWeek(oldStart);
547 dtStart.set(Calendar.SECOND, 0);
548 dtStart.set(Calendar.MILLISECOND, 0);
549 }
550
551
554 public void setDuration(Duration d) {
555 duration = (Duration)d.clone();
556 }
557
558
561 public void setFrequency(int freq) {
562 if ((frequency != DAILY) && (frequency != WEEKLY) &&
563 (frequency != MONTHLY) && (frequency != YEARLY) &&
564 (frequency != NO_RECURRENCE)) {
565
566 throw new IllegalArgumentException("Invalid frequency");
567 }
568
569 frequency = freq;
570 }
571
572
575 public void setInterval(int intr) {
576 interval = (intr > 0) ? intr : 1;
577 }
578
579
582 public void setOccurrence(int occur) {
583 occurrence = occur;
584 }
585
586
589 public void setUntil(Calendar u) {
590 if (u == null) {
591 until = null;
592
593 return;
594 }
595
596 until = (Calendar)u.clone();
597
598 until.clear(Calendar.ZONE_OFFSET);
599 until.clear(Calendar.DST_OFFSET);
600 until.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
601 }
602
603
606 public void setWeekStart(int weekstart) {
607 dtStart.setFirstDayOfWeek(weekstart);
608 }
609
610
615 @Override
616 public String toString() {
617 StringBundler sb = new StringBundler();
618
619 Class<?> clazz = getClass();
620
621 sb.append(clazz.getName());
622
623 sb.append("[dtStart=");
624 sb.append((dtStart != null) ? dtStart.toString() : "null");
625 sb.append(",duration=");
626 sb.append((duration != null) ? duration.toString() : "null");
627 sb.append(",frequency=");
628 sb.append(frequency);
629 sb.append(",interval=");
630 sb.append(interval);
631 sb.append(",until=");
632 sb.append((until != null) ? until.toString() : "null");
633 sb.append(",byDay=");
634
635 if (byDay == null) {
636 sb.append("null");
637 }
638 else {
639 sb.append("[");
640
641 for (int i = 0; i < byDay.length; i++) {
642 if (i != 0) {
643 sb.append(",");
644 }
645
646 if (byDay[i] != null) {
647 sb.append(byDay[i].toString());
648 }
649 else {
650 sb.append("null");
651 }
652 }
653
654 sb.append("]");
655 }
656
657 sb.append(",byMonthDay=");
658 sb.append(stringizeIntArray(byMonthDay));
659 sb.append(",byYearDay=");
660 sb.append(stringizeIntArray(byYearDay));
661 sb.append(",byWeekNo=");
662 sb.append(stringizeIntArray(byWeekNo));
663 sb.append(",byMonth=");
664 sb.append(stringizeIntArray(byMonth));
665 sb.append(']');
666
667 return sb.toString();
668 }
669
670
675 protected static long getDayNumber(Calendar cal) {
676 Calendar tempCal = (Calendar)cal.clone();
677
678
679
680 tempCal.set(Calendar.MILLISECOND, 0);
681 tempCal.set(Calendar.SECOND, 0);
682 tempCal.set(Calendar.MINUTE, 0);
683 tempCal.set(Calendar.HOUR_OF_DAY, 0);
684
685 return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
686 }
687
688
693 protected static long getMonthNumber(Calendar cal) {
694 return
695 ((cal.get(Calendar.YEAR) - 1970) * 12L) +
696 ((cal.get(Calendar.MONTH) - Calendar.JANUARY));
697 }
698
699
704 protected static long getWeekNumber(Calendar cal) {
705 Calendar tempCal = (Calendar)cal.clone();
706
707
708
709 tempCal.set(Calendar.MILLISECOND, 0);
710 tempCal.set(Calendar.SECOND, 0);
711 tempCal.set(Calendar.MINUTE, 0);
712 tempCal.set(Calendar.HOUR_OF_DAY, 0);
713
714
715
716 int delta =
717 tempCal.getFirstDayOfWeek() - tempCal.get(Calendar.DAY_OF_WEEK);
718
719 if (delta > 0) {
720 delta -= 7;
721 }
722
723
724
725
726
727
728 long weekEpoch =
729 (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24L * 60 * 60 *
730 1000;
731
732 return
733 (tempCal.getTime().getTime() - weekEpoch) /
734 (7 * 24 * 60 * 60 * 1000);
735 }
736
737
740 protected static void reduce_constant_length_field(
741 int field, Calendar start, Calendar candidate) {
742
743 if ((start.getMaximum(field) != start.getLeastMaximum(field)) ||
744 (start.getMinimum(field) != start.getGreatestMinimum(field))) {
745
746 throw new IllegalArgumentException("Not a constant length field");
747 }
748
749 int fieldLength =
750 (start.getMaximum(field) - start.getMinimum(field) + 1);
751 int delta = start.get(field) - candidate.get(field);
752
753 if (delta > 0) {
754 delta -= fieldLength;
755 }
756
757 candidate.add(field, delta);
758 }
759
760
763 protected static void reduce_day_of_month(
764 Calendar start, Calendar candidate) {
765
766 Calendar tempCal = (Calendar)candidate.clone();
767
768 tempCal.add(Calendar.MONTH, -1);
769
770 int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
771
772 if (delta > 0) {
773 delta -= tempCal.getActualMaximum(Calendar.DATE);
774 }
775
776 candidate.add(Calendar.DATE, delta);
777
778 while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
779 tempCal.add(Calendar.MONTH, -1);
780 candidate.add(
781 Calendar.DATE, -tempCal.getActualMaximum(Calendar.DATE));
782 }
783 }
784
785
788 protected static void reduce_day_of_year(
789 Calendar start, Calendar candidate) {
790
791 if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH)) ||
792 ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH)) &&
793 (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
794
795 candidate.add(Calendar.YEAR, -1);
796 }
797
798
799
800 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
801 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
802
803 while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH)) ||
804 (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
805
806 candidate.add(Calendar.YEAR, -1);
807 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
808 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
809 }
810 }
811
812
817 protected boolean candidateIsInRecurrence(
818 Calendar candidate, boolean debug) {
819
820 if ((until != null) &&
821 (candidate.getTime().getTime() > until.getTime().getTime())) {
822
823
824
825 if (debug) {
826 System.err.println("after until");
827 }
828
829 return false;
830 }
831
832 if ((getRecurrenceCount(candidate) % interval) != 0) {
833
834
835
836 if (debug) {
837 System.err.println("not an interval rep");
838 }
839
840 return false;
841 }
842 else if ((occurrence > 0) &&
843 (getRecurrenceCount(candidate) >= occurrence)) {
844
845 return false;
846 }
847
848 if (!matchesByDay(candidate) || !matchesByMonthDay(candidate) ||
849 !matchesByYearDay(candidate) || !matchesByWeekNo(candidate) ||
850 !matchesByMonth(candidate)) {
851
852
853
854 if (debug) {
855 System.err.println("doesn't match a by*");
856 }
857
858 return false;
859 }
860
861 if (debug) {
862 System.err.println("All checks succeeded");
863 }
864
865 return true;
866 }
867
868
873 protected int getMinimumInterval() {
874 if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null) ||
875 (byYearDay != null)) {
876
877 return DAILY;
878 }
879 else if ((frequency == WEEKLY) || (byWeekNo != null)) {
880 return WEEKLY;
881 }
882 else if ((frequency == MONTHLY) || (byMonth != null)) {
883 return MONTHLY;
884 }
885 else if (frequency == YEARLY) {
886 return YEARLY;
887 }
888 else if (frequency == NO_RECURRENCE) {
889 return NO_RECURRENCE;
890 }
891 else {
892
893
894
895 throw new IllegalStateException(
896 "Internal error: Unknown frequency value");
897 }
898 }
899
900
905 protected int getRecurrenceCount(Calendar candidate) {
906 switch (frequency) {
907
908 case NO_RECURRENCE :
909 return 0;
910
911 case DAILY :
912 return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
913
914 case WEEKLY :
915 Calendar tempCand = (Calendar)candidate.clone();
916
917 tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
918
919 return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
920
921 case MONTHLY :
922 return
923 (int)(getMonthNumber(candidate) - getMonthNumber(dtStart));
924
925 case YEARLY :
926 return
927 candidate.get(Calendar.YEAR) - dtStart.get(Calendar.YEAR);
928
929 default :
930 throw new IllegalStateException("bad frequency internally...");
931 }
932 }
933
934
939 protected boolean matchesByDay(Calendar candidate) {
940 if (ArrayUtil.isEmpty(byDay)) {
941
942
943
944 return true;
945 }
946
947 for (int i = 0; i < byDay.length; i++) {
948 if (matchesIndividualByDay(candidate, byDay[i])) {
949 return true;
950 }
951 }
952
953 return false;
954 }
955
956
961 protected boolean matchesByField(
962 int[] array, int field, Calendar candidate, boolean allowNegative) {
963
964 if (ArrayUtil.isEmpty(array)) {
965
966
967
968 return true;
969 }
970
971 for (int i = 0; i < array.length; i++) {
972 int val = 0;
973
974 if (allowNegative && (array[i] < 0)) {
975
976
977
978 int max = candidate.getActualMaximum(field);
979
980 val = (max + 1) + array[i];
981 }
982 else {
983 val = array[i];
984 }
985
986 if (val == candidate.get(field)) {
987 return true;
988 }
989 }
990
991 return false;
992 }
993
994
999 protected boolean matchesByMonth(Calendar candidate) {
1000 return matchesByField(byMonth, Calendar.MONTH, candidate, false);
1001 }
1002
1003
1008 protected boolean matchesByMonthDay(Calendar candidate) {
1009 return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
1010 }
1011
1012
1017 protected boolean matchesByWeekNo(Calendar candidate) {
1018 return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1019 }
1020
1021
1026 protected boolean matchesByYearDay(Calendar candidate) {
1027 return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1028 }
1029
1030
1035 protected boolean matchesIndividualByDay(
1036 Calendar candidate, DayAndPosition pos) {
1037
1038 if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
1039 return false;
1040 }
1041
1042 int position = pos.getDayPosition();
1043
1044 if (position == 0) {
1045 return true;
1046 }
1047
1048 int field = Calendar.DAY_OF_MONTH;
1049
1050 if (position > 0) {
1051 int candidatePosition = ((candidate.get(field) - 1) / 7) + 1;
1052
1053 if (position == candidatePosition) {
1054 return true;
1055 }
1056
1057 return false;
1058 }
1059 else {
1060
1061
1062
1063 int negativeCandidatePosition =
1064 ((candidate.getActualMaximum(field) - candidate.get(field)) /
1065 7) + 1;
1066
1067 if (-position == negativeCandidatePosition) {
1068 return true;
1069 }
1070
1071 return false;
1072 }
1073 }
1074
1075
1080 protected String stringizeIntArray(int[] a) {
1081 if (a == null) {
1082 return "null";
1083 }
1084
1085 StringBundler sb = new StringBundler(2 * a.length + 1);
1086
1087 sb.append("[");
1088
1089 for (int i = 0; i < a.length; i++) {
1090 if (i != 0) {
1091 sb.append(",");
1092 }
1093
1094 sb.append(a[i]);
1095 }
1096
1097 sb.append("]");
1098
1099 return sb.toString();
1100 }
1101
1102
1105 protected DayAndPosition[] byDay;
1106
1107
1110 protected int[] byMonth;
1111
1112
1115 protected int[] byMonthDay;
1116
1117
1120 protected int[] byWeekNo;
1121
1122
1125 protected int[] byYearDay;
1126
1127
1130 protected Calendar dtStart;
1131
1132
1135 protected Duration duration;
1136
1137
1140 protected int frequency;
1141
1142
1145 protected int interval;
1146
1147
1150 protected int occurrence;
1151
1152
1155 protected Calendar until;
1156
1157 }