001
014
015
044
045 package com.liferay.util.cal;
046
047 import com.liferay.portal.kernel.util.CalendarFactoryUtil;
048 import com.liferay.portal.kernel.util.StringBundler;
049 import com.liferay.portal.kernel.util.StringPool;
050 import com.liferay.portal.kernel.util.TimeZoneUtil;
051
052 import java.io.Serializable;
053
054 import java.util.Calendar;
055 import java.util.Date;
056
057
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
211 public Calendar getCandidateStartTime(Calendar current) {
212 if (dtStart.getTime().getTime() > current.getTime().getTime()) {
213 throw new IllegalArgumentException("Current time before DtStart");
214 }
215
216 int minInterval = getMinimumInterval();
217 Calendar candidate = (Calendar)current.clone();
218
219 if (true) {
220
221
222
223 candidate.clear(Calendar.ZONE_OFFSET);
224 candidate.clear(Calendar.DST_OFFSET);
225 candidate.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
226 candidate.setMinimalDaysInFirstWeek(4);
227 candidate.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
228 }
229
230 if (frequency == NO_RECURRENCE) {
231 candidate.setTime(dtStart.getTime());
232
233 return candidate;
234 }
235
236 reduce_constant_length_field(Calendar.SECOND, dtStart, candidate);
237 reduce_constant_length_field(Calendar.MINUTE, dtStart, candidate);
238 reduce_constant_length_field(Calendar.HOUR_OF_DAY, dtStart, candidate);
239
240 switch (minInterval) {
241
242 case DAILY :
243
244
245
246 break;
247
248 case WEEKLY :
249 reduce_constant_length_field(Calendar.DAY_OF_WEEK, dtStart,
250 candidate);
251 break;
252
253 case MONTHLY :
254 reduce_day_of_month(dtStart, candidate);
255 break;
256
257 case YEARLY :
258 reduce_day_of_year(dtStart, candidate);
259 break;
260 }
261
262 return candidate;
263 }
264
265
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
289 public Calendar getDtStart() {
290 return (Calendar)dtStart.clone();
291 }
292
293
298 public Duration getDuration() {
299 return (Duration)duration.clone();
300 }
301
302
307 public int getFrequency() {
308 return frequency;
309 }
310
311
316 public int getInterval() {
317 return interval;
318 }
319
320
325 public int getOccurrence() {
326 return occurrence;
327 }
328
329
334 public Calendar getUntil() {
335 return ((until != null) ? (Calendar)until.clone() : null);
336 }
337
338
343 public int getWeekStart() {
344 return dtStart.getFirstDayOfWeek();
345 }
346
347
352 public boolean isInRecurrence(Calendar current) {
353 return isInRecurrence(current, false);
354 }
355
356
361 public boolean isInRecurrence(Calendar current, boolean debug) {
362 Calendar myCurrent = (Calendar)current.clone();
363
364
365
366 myCurrent.clear(Calendar.ZONE_OFFSET);
367 myCurrent.clear(Calendar.DST_OFFSET);
368 myCurrent.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
369 myCurrent.setMinimalDaysInFirstWeek(4);
370 myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
371
372 if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
373
374
375
376 if (debug) {
377 System.err.println("current < start");
378 }
379
380 return false;
381 }
382
383 Calendar candidate = getCandidateStartTime(myCurrent);
384
385
386
387 while (candidate.getTime().getTime() + duration.getInterval()
388 > myCurrent.getTime().getTime()) {
389 if (candidateIsInRecurrence(candidate, debug)) {
390 return true;
391 }
392
393
394
395 candidate.add(Calendar.SECOND, -1);
396
397
398
399 if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
400 if (debug) {
401 System.err.println("No candidates after dtStart");
402 }
403
404 return false;
405 }
406
407 candidate = getCandidateStartTime(candidate);
408 }
409
410 if (debug) {
411 System.err.println("No matching candidates");
412 }
413
414 return false;
415 }
416
417
420 public void setByDay(DayAndPosition[] b) {
421 if (b == null) {
422 byDay = null;
423
424 return;
425 }
426
427 byDay = new DayAndPosition[b.length];
428
429
433 for (int i = 0; i < b.length; i++) {
434 byDay[i] = (DayAndPosition)b[i].clone();
435 }
436 }
437
438
441 public void setByMonth(int[] b) {
442 if (b == null) {
443 byMonth = null;
444
445 return;
446 }
447
448 byMonth = new int[b.length];
449
450 System.arraycopy(b, 0, byMonth, 0, b.length);
451 }
452
453
456 public void setByMonthDay(int[] b) {
457 if (b == null) {
458 byMonthDay = null;
459
460 return;
461 }
462
463 byMonthDay = new int[b.length];
464
465 System.arraycopy(b, 0, byMonthDay, 0, b.length);
466 }
467
468
471 public void setByWeekNo(int[] b) {
472 if (b == null) {
473 byWeekNo = null;
474
475 return;
476 }
477
478 byWeekNo = new int[b.length];
479
480 System.arraycopy(b, 0, byWeekNo, 0, b.length);
481 }
482
483
486 public void setByYearDay(int[] b) {
487 if (b == null) {
488 byYearDay = null;
489
490 return;
491 }
492
493 byYearDay = new int[b.length];
494
495 System.arraycopy(b, 0, byYearDay, 0, b.length);
496 }
497
498
501 public void setDtEnd(Calendar end) {
502 Calendar tempEnd = (Calendar)end.clone();
503
504 tempEnd.clear(Calendar.ZONE_OFFSET);
505 tempEnd.clear(Calendar.DST_OFFSET);
506 tempEnd.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
507 duration.setInterval(tempEnd.getTime().getTime()
508 - dtStart.getTime().getTime());
509 }
510
511
514 public void setDtStart(Calendar start) {
515 int oldStart;
516
517 if (dtStart != null) {
518 oldStart = dtStart.getFirstDayOfWeek();
519 }
520 else {
521 oldStart = Calendar.MONDAY;
522 }
523
524 if (start == null) {
525 dtStart = CalendarFactoryUtil.getCalendar(
526 TimeZoneUtil.getTimeZone(StringPool.UTC));
527
528 dtStart.setTime(new Date(0L));
529 }
530 else {
531 dtStart = (Calendar)start.clone();
532
533 dtStart.clear(Calendar.ZONE_OFFSET);
534 dtStart.clear(Calendar.DST_OFFSET);
535 dtStart.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
536 }
537
538 dtStart.setMinimalDaysInFirstWeek(4);
539 dtStart.setFirstDayOfWeek(oldStart);
540 }
541
542
545 public void setDuration(Duration d) {
546 duration = (Duration)d.clone();
547 }
548
549
552 public void setFrequency(int freq) {
553 if ((frequency != DAILY) && (frequency != WEEKLY)
554 && (frequency != MONTHLY) && (frequency != YEARLY)
555 && (frequency != NO_RECURRENCE)) {
556 throw new IllegalArgumentException("Invalid frequency");
557 }
558
559 frequency = freq;
560 }
561
562
565 public void setInterval(int intr) {
566 interval = (intr > 0) ? intr : 1;
567 }
568
569
572 public void setOccurrence(int occur) {
573 occurrence = occur;
574 }
575
576
579 public void setUntil(Calendar u) {
580 if (u == null) {
581 until = null;
582
583 return;
584 }
585
586 until = (Calendar)u.clone();
587
588 until.clear(Calendar.ZONE_OFFSET);
589 until.clear(Calendar.DST_OFFSET);
590 until.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
591 }
592
593
596 public void setWeekStart(int weekstart) {
597 dtStart.setFirstDayOfWeek(weekstart);
598 }
599
600
605 @Override
606 public String toString() {
607 StringBundler sb = new StringBundler();
608
609 sb.append(getClass().getName());
610 sb.append("[dtStart=");
611 sb.append((dtStart != null) ? dtStart.toString() : "null");
612 sb.append(",duration=");
613 sb.append((duration != null) ? duration.toString() : "null");
614 sb.append(",frequency=");
615 sb.append(frequency);
616 sb.append(",interval=");
617 sb.append(interval);
618 sb.append(",until=");
619 sb.append((until != null) ? until.toString() : "null");
620 sb.append(",byDay=");
621
622 if (byDay == null) {
623 sb.append("null");
624 }
625 else {
626 sb.append("[");
627
628 for (int i = 0; i < byDay.length; i++) {
629 if (i != 0) {
630 sb.append(",");
631 }
632
633 if (byDay[i] != null) {
634 sb.append(byDay[i].toString());
635 }
636 else {
637 sb.append("null");
638 }
639 }
640
641 sb.append("]");
642 }
643
644 sb.append(",byMonthDay=");
645 sb.append(stringizeIntArray(byMonthDay));
646 sb.append(",byYearDay=");
647 sb.append(stringizeIntArray(byYearDay));
648 sb.append(",byWeekNo=");
649 sb.append(stringizeIntArray(byWeekNo));
650 sb.append(",byMonth=");
651 sb.append(stringizeIntArray(byMonth));
652 sb.append(']');
653
654 return sb.toString();
655 }
656
657
662 protected static long getDayNumber(Calendar cal) {
663 Calendar tempCal = (Calendar)cal.clone();
664
665
666
667 tempCal.set(Calendar.MILLISECOND, 0);
668 tempCal.set(Calendar.SECOND, 0);
669 tempCal.set(Calendar.MINUTE, 0);
670 tempCal.set(Calendar.HOUR_OF_DAY, 0);
671
672 return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
673 }
674
675
680 protected static long getMonthNumber(Calendar cal) {
681 return (cal.get(Calendar.YEAR) - 1970) * 12L
682 + (cal.get(Calendar.MONTH) - Calendar.JANUARY);
683 }
684
685
690 protected static long getWeekNumber(Calendar cal) {
691 Calendar tempCal = (Calendar)cal.clone();
692
693
694
695 tempCal.set(Calendar.MILLISECOND, 0);
696 tempCal.set(Calendar.SECOND, 0);
697 tempCal.set(Calendar.MINUTE, 0);
698 tempCal.set(Calendar.HOUR_OF_DAY, 0);
699
700
701
702 int delta = tempCal.getFirstDayOfWeek()
703 - tempCal.get(Calendar.DAY_OF_WEEK);
704
705 if (delta > 0) {
706 delta -= 7;
707 }
708
709
710
711
712
713
714 long weekEpoch = (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24L
715 * 60 * 60 * 1000L;
716
717 return (tempCal.getTime().getTime() - weekEpoch)
718 / (7 * 24 * 60 * 60 * 1000);
719 }
720
721
724 protected static void reduce_constant_length_field(int field,
725 Calendar start,
726 Calendar candidate) {
727 if ((start.getMaximum(field) != start.getLeastMaximum(field))
728 || (start.getMinimum(field) != start.getGreatestMinimum(field))) {
729 throw new IllegalArgumentException("Not a constant length field");
730 }
731
732 int fieldLength = (start.getMaximum(field) - start.getMinimum(field)
733 + 1);
734 int delta = start.get(field) - candidate.get(field);
735
736 if (delta > 0) {
737 delta -= fieldLength;
738 }
739
740 candidate.add(field, delta);
741 }
742
743
746 protected static void reduce_day_of_month(Calendar start,
747 Calendar candidate) {
748 Calendar tempCal = (Calendar)candidate.clone();
749
750 tempCal.add(Calendar.MONTH, -1);
751
752 int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
753
754 if (delta > 0) {
755 delta -= tempCal.getActualMaximum(Calendar.DATE);
756 }
757
758 candidate.add(Calendar.DATE, delta);
759
760 while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
761 tempCal.add(Calendar.MONTH, -1);
762 candidate.add(Calendar.DATE,
763 -tempCal.getActualMaximum(Calendar.DATE));
764 }
765 }
766
767
770 protected static void reduce_day_of_year(Calendar start,
771 Calendar candidate) {
772 if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH))
773 || ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH))
774 && (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
775 candidate.add(Calendar.YEAR, -1);
776 }
777
778
779
780 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
781 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
782
783 while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH))
784 || (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
785 candidate.add(Calendar.YEAR, -1);
786 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
787 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
788 }
789 }
790
791
796 protected boolean candidateIsInRecurrence(Calendar candidate,
797 boolean debug) {
798 if ((until != null)
799 && (candidate.getTime().getTime() > until.getTime().getTime())) {
800
801
802
803 if (debug) {
804 System.err.println("after until");
805 }
806
807 return false;
808 }
809
810 if (getRecurrenceCount(candidate) % interval != 0) {
811
812
813
814 if (debug) {
815 System.err.println("not an interval rep");
816 }
817
818 return false;
819 }
820 else if ((occurrence > 0) &&
821 (getRecurrenceCount(candidate) >= occurrence)) {
822
823 return false;
824 }
825
826 if (!matchesByDay(candidate) ||!matchesByMonthDay(candidate)
827 ||!matchesByYearDay(candidate) ||!matchesByWeekNo(candidate)
828 ||!matchesByMonth(candidate)) {
829
830
831
832 if (debug) {
833 System.err.println("doesn't match a by*");
834 }
835
836 return false;
837 }
838
839 if (debug) {
840 System.err.println("All checks succeeded");
841 }
842
843 return true;
844 }
845
846
851 protected int getMinimumInterval() {
852 if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null)
853 || (byYearDay != null)) {
854 return DAILY;
855 }
856 else if ((frequency == WEEKLY) || (byWeekNo != null)) {
857 return WEEKLY;
858 }
859 else if ((frequency == MONTHLY) || (byMonth != null)) {
860 return MONTHLY;
861 }
862 else if (frequency == YEARLY) {
863 return YEARLY;
864 }
865 else if (frequency == NO_RECURRENCE) {
866 return NO_RECURRENCE;
867 }
868 else {
869
870
871
872 throw new IllegalStateException(
873 "Internal error: Unknown frequency value");
874 }
875 }
876
877
882 protected int getRecurrenceCount(Calendar candidate) {
883 switch (frequency) {
884
885 case NO_RECURRENCE :
886 return 0;
887
888 case DAILY :
889 return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
890
891 case WEEKLY :
892 Calendar tempCand = (Calendar)candidate.clone();
893
894 tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
895
896 return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
897
898 case MONTHLY :
899 return (int)(getMonthNumber(candidate)
900 - getMonthNumber(dtStart));
901
902 case YEARLY :
903 return candidate.get(Calendar.YEAR)
904 - dtStart.get(Calendar.YEAR);
905
906 default :
907 throw new IllegalStateException("bad frequency internally...");
908 }
909 }
910
911
916 protected boolean matchesByDay(Calendar candidate) {
917 if ((byDay == null) || (byDay.length == 0)) {
918
919
920
921 return true;
922 }
923
924 int i;
925
926 for (i = 0; i < byDay.length; i++) {
927 if (matchesIndividualByDay(candidate, byDay[i])) {
928 return true;
929 }
930 }
931
932 return false;
933 }
934
935
940 protected boolean matchesByField(int[] array, int field,
941 Calendar candidate,
942 boolean allowNegative) {
943 if ((array == null) || (array.length == 0)) {
944
945
946
947 return true;
948 }
949
950 int i;
951
952 for (i = 0; i < array.length; i++) {
953 int val;
954
955 if (allowNegative && (array[i] < 0)) {
956
957
958
959 int max = candidate.getActualMaximum(field);
960
961 val = (max + 1) + array[i];
962 }
963 else {
964 val = array[i];
965 }
966
967 if (val == candidate.get(field)) {
968 return true;
969 }
970 }
971
972 return false;
973 }
974
975
980 protected boolean matchesByMonth(Calendar candidate) {
981 return matchesByField(byMonth, Calendar.MONTH, candidate, false);
982 }
983
984
989 protected boolean matchesByMonthDay(Calendar candidate) {
990 return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
991 }
992
993
998 protected boolean matchesByWeekNo(Calendar candidate) {
999 return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1000 }
1001
1002
1007 protected boolean matchesByYearDay(Calendar candidate) {
1008 return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1009 }
1010
1011
1016 protected boolean matchesIndividualByDay(Calendar candidate,
1017 DayAndPosition pos) {
1018 if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
1019 return false;
1020 }
1021
1022 int position = pos.getDayPosition();
1023
1024 if (position == 0) {
1025 return true;
1026 }
1027
1028 int field = Calendar.DAY_OF_MONTH;
1029
1030 if (position > 0) {
1031 int candidatePosition = ((candidate.get(field) - 1) / 7) + 1;
1032
1033 return (position == candidatePosition);
1034 }
1035 else {
1036
1037
1038
1039 int negativeCandidatePosition =
1040 ((candidate.getActualMaximum(field) - candidate.get(field)) / 7)
1041 + 1;
1042
1043 return (-position == negativeCandidatePosition);
1044 }
1045 }
1046
1047
1052 protected String stringizeIntArray(int[] a) {
1053 if (a == null) {
1054 return "null";
1055 }
1056
1057 StringBundler sb = new StringBundler(2 * a.length + 1);
1058
1059 sb.append("[");
1060
1061 for (int i = 0; i < a.length; i++) {
1062 if (i != 0) {
1063 sb.append(",");
1064 }
1065
1066 sb.append(a[i]);
1067 }
1068
1069 sb.append("]");
1070
1071 return sb.toString();
1072 }
1073
1074
1077 protected DayAndPosition[] byDay;
1078
1079
1082 protected int[] byMonth;
1083
1084
1087 protected int[] byMonthDay;
1088
1089
1092 protected int[] byWeekNo;
1093
1094
1097 protected int[] byYearDay;
1098
1099
1102 protected Calendar dtStart;
1103
1104
1107 protected Duration duration;
1108
1109
1112 protected int frequency;
1113
1114
1117 protected int interval;
1118
1119
1122 protected int occurrence = 0;
1123
1124
1127 protected Calendar until;
1128
1129 }