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