001
014
015
044
045 package com.liferay.portal.kernel.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
060 public class Recurrence implements Serializable {
061
062
065 public static final int DAILY = 3;
066
067
070 public static final int MONTHLY = 5;
071
072
075 public static final int NO_RECURRENCE = 7;
076
077
080 public static final int WEEKLY = 4;
081
082
085 public static final int YEARLY = 6;
086
087
090 public Recurrence() {
091 this(null, new Duration(), NO_RECURRENCE);
092 }
093
094
097 public Recurrence(Calendar start, Duration dur) {
098 this(start, dur, NO_RECURRENCE);
099 }
100
101
104 public Recurrence(Calendar start, Duration dur, int freq) {
105 setDtStart(start);
106
107 duration = (Duration)dur.clone();
108 frequency = freq;
109 interval = 1;
110 }
111
112
113
114
119 public DayAndPosition[] getByDay() {
120 if (byDay == null) {
121 return null;
122 }
123
124 DayAndPosition[] b = new DayAndPosition[byDay.length];
125
126
130 for (int i = 0; i < byDay.length; i++) {
131 b[i] = (DayAndPosition)byDay[i].clone();
132 }
133
134 return b;
135 }
136
137
142 public int[] getByMonth() {
143 if (byMonth == null) {
144 return null;
145 }
146
147 int[] b = new int[byMonth.length];
148
149 System.arraycopy(byMonth, 0, b, 0, byMonth.length);
150
151 return b;
152 }
153
154
159 public int[] getByMonthDay() {
160 if (byMonthDay == null) {
161 return null;
162 }
163
164 int[] b = new int[byMonthDay.length];
165
166 System.arraycopy(byMonthDay, 0, b, 0, byMonthDay.length);
167
168 return b;
169 }
170
171
176 public int[] getByWeekNo() {
177 if (byWeekNo == null) {
178 return null;
179 }
180
181 int[] b = new int[byWeekNo.length];
182
183 System.arraycopy(byWeekNo, 0, b, 0, byWeekNo.length);
184
185 return b;
186 }
187
188
193 public int[] getByYearDay() {
194 if (byYearDay == null) {
195 return null;
196 }
197
198 int[] b = new int[byYearDay.length];
199
200 System.arraycopy(byYearDay, 0, b, 0, byYearDay.length);
201
202 return b;
203 }
204
205
210 public Calendar getCandidateStartTime(Calendar current) {
211 if (dtStart.getTime().getTime() > current.getTime().getTime()) {
212 throw new IllegalArgumentException("Current time before DtStart");
213 }
214
215 int minInterval = getMinimumInterval();
216 Calendar candidate = (Calendar)current.clone();
217
218 if (true) {
219
220
221
222 candidate.clear(Calendar.ZONE_OFFSET);
223 candidate.clear(Calendar.DST_OFFSET);
224 candidate.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
225 candidate.setMinimalDaysInFirstWeek(4);
226 candidate.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
227 }
228
229 if (frequency == NO_RECURRENCE) {
230 candidate.setTime(dtStart.getTime());
231
232 return candidate;
233 }
234
235 reduce_constant_length_field(Calendar.SECOND, dtStart, candidate);
236 reduce_constant_length_field(Calendar.MINUTE, dtStart, candidate);
237 reduce_constant_length_field(Calendar.HOUR_OF_DAY, dtStart, candidate);
238
239 switch (minInterval) {
240
241 case DAILY :
242
243
244
245 break;
246
247 case WEEKLY :
248 reduce_constant_length_field(
249 Calendar.DAY_OF_WEEK, dtStart, candidate);
250 break;
251
252 case MONTHLY :
253 reduce_day_of_month(dtStart, candidate);
254 break;
255
256 case YEARLY :
257 reduce_day_of_year(dtStart, candidate);
258 break;
259 }
260
261 return candidate;
262 }
263
264
269 public Calendar getDtEnd() {
270
271
275 Calendar tempEnd = (Calendar)dtStart.clone();
276
277 tempEnd.setTime(
278 new Date(dtStart.getTime().getTime() + duration.getInterval()));
279
280 return tempEnd;
281 }
282
283
288 public Calendar getDtStart() {
289 return (Calendar)dtStart.clone();
290 }
291
292
297 public Duration getDuration() {
298 return (Duration)duration.clone();
299 }
300
301
306 public int getFrequency() {
307 return frequency;
308 }
309
310
315 public int getInterval() {
316 return interval;
317 }
318
319
324 public int getOccurrence() {
325 return occurrence;
326 }
327
328
333 public Calendar getUntil() {
334 return ((until != null) ? (Calendar)until.clone() : null);
335 }
336
337
342 public int getWeekStart() {
343 return dtStart.getFirstDayOfWeek();
344 }
345
346
351 public boolean isInRecurrence(Calendar current) {
352 return isInRecurrence(current, false);
353 }
354
355
360 public boolean isInRecurrence(Calendar current, boolean debug) {
361 Calendar myCurrent = (Calendar)current.clone();
362
363
364
365 myCurrent.clear(Calendar.ZONE_OFFSET);
366 myCurrent.clear(Calendar.DST_OFFSET);
367 myCurrent.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
368 myCurrent.setMinimalDaysInFirstWeek(4);
369 myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
370
371 if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
372
373
374
375 if (debug) {
376 System.err.println("current < start");
377 }
378
379 return false;
380 }
381
382 Calendar candidate = getCandidateStartTime(myCurrent);
383
384
385
386 while ((candidate.getTime().getTime() + duration.getInterval()) >
387 myCurrent.getTime().getTime()) {
388
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(
508 tempEnd.getTime().getTime() - 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
682 ((cal.get(Calendar.YEAR) - 1970) * 12L) +
683 ((cal.get(Calendar.MONTH) - Calendar.JANUARY));
684 }
685
686
691 protected static long getWeekNumber(Calendar cal) {
692 Calendar tempCal = (Calendar)cal.clone();
693
694
695
696 tempCal.set(Calendar.MILLISECOND, 0);
697 tempCal.set(Calendar.SECOND, 0);
698 tempCal.set(Calendar.MINUTE, 0);
699 tempCal.set(Calendar.HOUR_OF_DAY, 0);
700
701
702
703 int delta = tempCal.getFirstDayOfWeek()
704 - tempCal.get(Calendar.DAY_OF_WEEK);
705
706 if (delta > 0) {
707 delta -= 7;
708 }
709
710
711
712
713
714
715 long weekEpoch =
716 (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24L * 60 * 60 *
717 1000;
718
719 return
720 (tempCal.getTime().getTime() - weekEpoch) /
721 (7 * 24 * 60 * 60 * 1000);
722 }
723
724
727 protected static void reduce_constant_length_field(
728 int field, Calendar start, Calendar candidate) {
729
730 if ((start.getMaximum(field) != start.getLeastMaximum(field))
731 || (start.getMinimum(field) != start.getGreatestMinimum(field))) {
732 throw new IllegalArgumentException("Not a constant length field");
733 }
734
735 int fieldLength =
736 (start.getMaximum(field) - start.getMinimum(field) + 1);
737 int delta = start.get(field) - candidate.get(field);
738
739 if (delta > 0) {
740 delta -= fieldLength;
741 }
742
743 candidate.add(field, delta);
744 }
745
746
749 protected static void reduce_day_of_month(
750 Calendar start, Calendar candidate) {
751
752 Calendar tempCal = (Calendar)candidate.clone();
753
754 tempCal.add(Calendar.MONTH, -1);
755
756 int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
757
758 if (delta > 0) {
759 delta -= tempCal.getActualMaximum(Calendar.DATE);
760 }
761
762 candidate.add(Calendar.DATE, delta);
763
764 while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
765 tempCal.add(Calendar.MONTH, -1);
766 candidate.add(
767 Calendar.DATE, -tempCal.getActualMaximum(Calendar.DATE));
768 }
769 }
770
771
774 protected static void reduce_day_of_year(
775 Calendar start, Calendar candidate) {
776
777 if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH))
778 || ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH))
779 && (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
780 candidate.add(Calendar.YEAR, -1);
781 }
782
783
784
785 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
786 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
787
788 while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH)) ||
789 (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
790
791 candidate.add(Calendar.YEAR, -1);
792 candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
793 candidate.set(Calendar.DATE, start.get(Calendar.DATE));
794 }
795 }
796
797
802 protected boolean candidateIsInRecurrence(
803 Calendar candidate, boolean debug) {
804
805 if ((until != null)
806 && (candidate.getTime().getTime() > until.getTime().getTime())) {
807
808
809
810 if (debug) {
811 System.err.println("after until");
812 }
813
814 return false;
815 }
816
817 if ((getRecurrenceCount(candidate) % interval) != 0) {
818
819
820
821 if (debug) {
822 System.err.println("not an interval rep");
823 }
824
825 return false;
826 }
827 else if ((occurrence > 0) &&
828 (getRecurrenceCount(candidate) >= occurrence)) {
829
830 return false;
831 }
832
833 if (!matchesByDay(candidate) ||!matchesByMonthDay(candidate)
834 ||!matchesByYearDay(candidate) ||!matchesByWeekNo(candidate)
835 ||!matchesByMonth(candidate)) {
836
837
838
839 if (debug) {
840 System.err.println("doesn't match a by*");
841 }
842
843 return false;
844 }
845
846 if (debug) {
847 System.err.println("All checks succeeded");
848 }
849
850 return true;
851 }
852
853
858 protected int getMinimumInterval() {
859 if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null)
860 || (byYearDay != null)) {
861 return DAILY;
862 }
863 else if ((frequency == WEEKLY) || (byWeekNo != null)) {
864 return WEEKLY;
865 }
866 else if ((frequency == MONTHLY) || (byMonth != null)) {
867 return MONTHLY;
868 }
869 else if (frequency == YEARLY) {
870 return YEARLY;
871 }
872 else if (frequency == NO_RECURRENCE) {
873 return NO_RECURRENCE;
874 }
875 else {
876
877
878
879 throw new IllegalStateException(
880 "Internal error: Unknown frequency value");
881 }
882 }
883
884
889 protected int getRecurrenceCount(Calendar candidate) {
890 switch (frequency) {
891
892 case NO_RECURRENCE :
893 return 0;
894
895 case DAILY :
896 return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
897
898 case WEEKLY :
899 Calendar tempCand = (Calendar)candidate.clone();
900
901 tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
902
903 return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
904
905 case MONTHLY :
906 return
907 (int)(getMonthNumber(candidate) - getMonthNumber(dtStart));
908
909 case YEARLY :
910 return
911 candidate.get(Calendar.YEAR) - dtStart.get(Calendar.YEAR);
912
913 default :
914 throw new IllegalStateException("bad frequency internally...");
915 }
916 }
917
918
923 protected boolean matchesByDay(Calendar candidate) {
924 if ((byDay == null) || (byDay.length == 0)) {
925
926
927
928 return true;
929 }
930
931 int i;
932
933 for (i = 0; i < byDay.length; i++) {
934 if (matchesIndividualByDay(candidate, byDay[i])) {
935 return true;
936 }
937 }
938
939 return false;
940 }
941
942
947 protected boolean matchesByField(
948 int[] array, int field, Calendar candidate, boolean allowNegative) {
949
950 if ((array == null) || (array.length == 0)) {
951
952
953
954 return true;
955 }
956
957 int i;
958
959 for (i = 0; i < array.length; i++) {
960 int val;
961
962 if (allowNegative && (array[i] < 0)) {
963
964
965
966 int max = candidate.getActualMaximum(field);
967
968 val = (max + 1) + array[i];
969 }
970 else {
971 val = array[i];
972 }
973
974 if (val == candidate.get(field)) {
975 return true;
976 }
977 }
978
979 return false;
980 }
981
982
987 protected boolean matchesByMonth(Calendar candidate) {
988 return matchesByField(byMonth, Calendar.MONTH, candidate, false);
989 }
990
991
996 protected boolean matchesByMonthDay(Calendar candidate) {
997 return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
998 }
999
1000
1005 protected boolean matchesByWeekNo(Calendar candidate) {
1006 return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1007 }
1008
1009
1014 protected boolean matchesByYearDay(Calendar candidate) {
1015 return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1016 }
1017
1018
1023 protected boolean matchesIndividualByDay(
1024 Calendar candidate, DayAndPosition pos) {
1025
1026 if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
1027 return false;
1028 }
1029
1030 int position = pos.getDayPosition();
1031
1032 if (position == 0) {
1033 return true;
1034 }
1035
1036 int field = Calendar.DAY_OF_MONTH;
1037
1038 if (position > 0) {
1039 int candidatePosition = ((candidate.get(field) - 1) / 7) + 1;
1040
1041 return (position == candidatePosition);
1042 }
1043 else {
1044
1045
1046
1047 int negativeCandidatePosition =
1048 ((candidate.getActualMaximum(field) - candidate.get(field)) / 7)
1049 + 1;
1050
1051 return (-position == negativeCandidatePosition);
1052 }
1053 }
1054
1055
1060 protected String stringizeIntArray(int[] a) {
1061 if (a == null) {
1062 return "null";
1063 }
1064
1065 StringBundler sb = new StringBundler(2 * a.length + 1);
1066
1067 sb.append("[");
1068
1069 for (int i = 0; i < a.length; i++) {
1070 if (i != 0) {
1071 sb.append(",");
1072 }
1073
1074 sb.append(a[i]);
1075 }
1076
1077 sb.append("]");
1078
1079 return sb.toString();
1080 }
1081
1082
1085 protected DayAndPosition[] byDay;
1086
1087
1090 protected int[] byMonth;
1091
1092
1095 protected int[] byMonthDay;
1096
1097
1100 protected int[] byWeekNo;
1101
1102
1105 protected int[] byYearDay;
1106
1107
1110 protected Calendar dtStart;
1111
1112
1115 protected Duration duration;
1116
1117
1120 protected int frequency;
1121
1122
1125 protected int interval;
1126
1127
1130 protected int occurrence = 0;
1131
1132
1135 protected Calendar until;
1136
1137 }