src/net/fortuna/ical4j/model/Recur.java

changeset 0
fb9019fb1bf7
child 3
73bdfa70b04e
equal deleted inserted replaced
-1:000000000000 0:2b633facb90b
1 /**
2 * Copyright (c) 2012, Ben Fortuna
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * o Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *
12 * o Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
15 *
16 * o Neither the name of Ben Fortuna nor the names of any other contributors
17 * may be used to endorse or promote products derived from this software
18 * without specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 */
32 package net.fortuna.ical4j.model;
33
34 import java.io.IOException;
35 import java.io.Serializable;
36 import java.text.ParseException;
37 import java.util.Calendar;
38 import java.util.Collections;
39 import java.util.HashMap;
40 import java.util.Iterator;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.NoSuchElementException;
44 import java.util.StringTokenizer;
45
46 import net.fortuna.ical4j.model.parameter.Value;
47 import net.fortuna.ical4j.util.Configurator;
48 import net.fortuna.ical4j.util.Dates;
49
50 import org.apache.commons.logging.Log;
51 import org.apache.commons.logging.LogFactory;
52
53 /**
54 * $Id$ [18-Apr-2004]
55 *
56 * Defines a recurrence.
57 * @version 2.0
58 * @author Ben Fortuna
59 */
60 public class Recur implements Serializable {
61
62 private static final long serialVersionUID = -7333226591784095142L;
63
64 private static final String FREQ = "FREQ";
65
66 private static final String UNTIL = "UNTIL";
67
68 private static final String COUNT = "COUNT";
69
70 private static final String INTERVAL = "INTERVAL";
71
72 private static final String BYSECOND = "BYSECOND";
73
74 private static final String BYMINUTE = "BYMINUTE";
75
76 private static final String BYHOUR = "BYHOUR";
77
78 private static final String BYDAY = "BYDAY";
79
80 private static final String BYMONTHDAY = "BYMONTHDAY";
81
82 private static final String BYYEARDAY = "BYYEARDAY";
83
84 private static final String BYWEEKNO = "BYWEEKNO";
85
86 private static final String BYMONTH = "BYMONTH";
87
88 private static final String BYSETPOS = "BYSETPOS";
89
90 private static final String WKST = "WKST";
91
92 /**
93 * Second frequency resolution.
94 */
95 public static final String SECONDLY = "SECONDLY";
96
97 /**
98 * Minute frequency resolution.
99 */
100 public static final String MINUTELY = "MINUTELY";
101
102 /**
103 * Hour frequency resolution.
104 */
105 public static final String HOURLY = "HOURLY";
106
107 /**
108 * Day frequency resolution.
109 */
110 public static final String DAILY = "DAILY";
111
112 /**
113 * Week frequency resolution.
114 */
115 public static final String WEEKLY = "WEEKLY";
116
117 /**
118 * Month frequency resolution.
119 */
120 public static final String MONTHLY = "MONTHLY";
121
122 /**
123 * Year frequency resolution.
124 */
125 public static final String YEARLY = "YEARLY";
126
127 /**
128 * When calculating dates matching this recur ({@code getDates()} or {@code getNextDate}),
129 * this property defines the maximum number of attempt to find a matching date by
130 * incrementing the seed.
131 * <p>The default value is 1000. A value of -1 corresponds to no maximum.</p>
132 */
133 public static final String KEY_MAX_INCREMENT_COUNT = "net.fortuna.ical4j.recur.maxincrementcount";
134
135 private static int maxIncrementCount;
136 static {
137 final String value = Configurator.getProperty(KEY_MAX_INCREMENT_COUNT);
138 if (value != null && value.length() > 0) {
139 maxIncrementCount = Integer.parseInt(value);
140 } else {
141 maxIncrementCount = 1000;
142 }
143 }
144
145 private transient Log log = LogFactory.getLog(Recur.class);
146
147 private String frequency;
148
149 private Date until;
150
151 private int count = -1;
152
153 private int interval = -1;
154
155 private NumberList secondList;
156
157 private NumberList minuteList;
158
159 private NumberList hourList;
160
161 private WeekDayList dayList;
162
163 private NumberList monthDayList;
164
165 private NumberList yearDayList;
166
167 private NumberList weekNoList;
168
169 private NumberList monthList;
170
171 private NumberList setPosList;
172
173 private String weekStartDay;
174
175 private Map experimentalValues = new HashMap();
176
177 // Calendar field we increment based on frequency.
178 private int calIncField;
179
180 /**
181 * Default constructor.
182 */
183 public Recur() {
184 }
185
186 /**
187 * Constructs a new instance from the specified string value.
188 * @param aValue a string representation of a recurrence.
189 * @throws ParseException thrown when the specified string contains an invalid representation of an UNTIL date value
190 */
191 public Recur(final String aValue) throws ParseException {
192 final StringTokenizer t = new StringTokenizer(aValue, ";=");
193 while (t.hasMoreTokens()) {
194 final String token = t.nextToken();
195 if (FREQ.equals(token)) {
196 frequency = nextToken(t, token);
197 }
198 else if (UNTIL.equals(token)) {
199 final String untilString = nextToken(t, token);
200 if (untilString != null && untilString.indexOf("T") >= 0) {
201 until = new DateTime(untilString);
202 // UNTIL must be specified in UTC time..
203 ((DateTime) until).setUtc(true);
204 }
205 else {
206 until = new Date(untilString);
207 }
208 }
209 else if (COUNT.equals(token)) {
210 count = Integer.parseInt(nextToken(t, token));
211 }
212 else if (INTERVAL.equals(token)) {
213 interval = Integer.parseInt(nextToken(t, token));
214 }
215 else if (BYSECOND.equals(token)) {
216 secondList = new NumberList(nextToken(t, token), 0, 59, false);
217 }
218 else if (BYMINUTE.equals(token)) {
219 minuteList = new NumberList(nextToken(t, token), 0, 59, false);
220 }
221 else if (BYHOUR.equals(token)) {
222 hourList = new NumberList(nextToken(t, token), 0, 23, false);
223 }
224 else if (BYDAY.equals(token)) {
225 dayList = new WeekDayList(nextToken(t, token));
226 }
227 else if (BYMONTHDAY.equals(token)) {
228 monthDayList = new NumberList(nextToken(t, token), 1, 31, true);
229 }
230 else if (BYYEARDAY.equals(token)) {
231 yearDayList = new NumberList(nextToken(t, token), 1, 366, true);
232 }
233 else if (BYWEEKNO.equals(token)) {
234 weekNoList = new NumberList(nextToken(t, token), 1, 53, true);
235 }
236 else if (BYMONTH.equals(token)) {
237 monthList = new NumberList(nextToken(t, token), 1, 12, false);
238 }
239 else if (BYSETPOS.equals(token)) {
240 setPosList = new NumberList(nextToken(t, token), 1, 366, true);
241 }
242 else if (WKST.equals(token)) {
243 weekStartDay = nextToken(t, token);
244 }
245 // assume experimental value..
246 else {
247 experimentalValues.put(token, nextToken(t, token));
248 }
249 }
250 validateFrequency();
251 }
252
253 private String nextToken(StringTokenizer t, String lastToken) {
254 try {
255 return t.nextToken();
256 }
257 catch (NoSuchElementException e) {
258 throw new IllegalArgumentException("Missing expected token, last token: " + lastToken);
259 }
260 }
261
262 /**
263 * @param frequency a recurrence frequency string
264 * @param until maximum recurrence date
265 */
266 public Recur(final String frequency, final Date until) {
267 this.frequency = frequency;
268 this.until = until;
269 validateFrequency();
270 }
271
272 /**
273 * @param frequency a recurrence frequency string
274 * @param count maximum recurrence count
275 */
276 public Recur(final String frequency, final int count) {
277 this.frequency = frequency;
278 this.count = count;
279 validateFrequency();
280 }
281
282 /**
283 * @return Returns the dayList.
284 */
285 public final WeekDayList getDayList() {
286 if (dayList == null) {
287 dayList = new WeekDayList();
288 }
289 return dayList;
290 }
291
292 /**
293 * @return Returns the hourList.
294 */
295 public final NumberList getHourList() {
296 if (hourList == null) {
297 hourList = new NumberList(0, 23, false);
298 }
299 return hourList;
300 }
301
302 /**
303 * @return Returns the minuteList.
304 */
305 public final NumberList getMinuteList() {
306 if (minuteList == null) {
307 minuteList = new NumberList(0, 59, false);
308 }
309 return minuteList;
310 }
311
312 /**
313 * @return Returns the monthDayList.
314 */
315 public final NumberList getMonthDayList() {
316 if (monthDayList == null) {
317 monthDayList = new NumberList(1, 31, true);
318 }
319 return monthDayList;
320 }
321
322 /**
323 * @return Returns the monthList.
324 */
325 public final NumberList getMonthList() {
326 if (monthList == null) {
327 monthList = new NumberList(1, 12, false);
328 }
329 return monthList;
330 }
331
332 /**
333 * @return Returns the secondList.
334 */
335 public final NumberList getSecondList() {
336 if (secondList == null) {
337 secondList = new NumberList(0, 59, false);
338 }
339 return secondList;
340 }
341
342 /**
343 * @return Returns the setPosList.
344 */
345 public final NumberList getSetPosList() {
346 if (setPosList == null) {
347 setPosList = new NumberList(1, 366, true);
348 }
349 return setPosList;
350 }
351
352 /**
353 * @return Returns the weekNoList.
354 */
355 public final NumberList getWeekNoList() {
356 if (weekNoList == null) {
357 weekNoList = new NumberList(1, 53, true);
358 }
359 return weekNoList;
360 }
361
362 /**
363 * @return Returns the yearDayList.
364 */
365 public final NumberList getYearDayList() {
366 if (yearDayList == null) {
367 yearDayList = new NumberList(1, 366, true);
368 }
369 return yearDayList;
370 }
371
372 /**
373 * @return Returns the count or -1 if the rule does not have a count.
374 */
375 public final int getCount() {
376 return count;
377 }
378
379 /**
380 * @return Returns the experimentalValues.
381 */
382 public final Map getExperimentalValues() {
383 return experimentalValues;
384 }
385
386 /**
387 * @return Returns the frequency.
388 */
389 public final String getFrequency() {
390 return frequency;
391 }
392
393 /**
394 * @return Returns the interval or -1 if the rule does not have an interval defined.
395 */
396 public final int getInterval() {
397 return interval;
398 }
399
400 /**
401 * @return Returns the until or null if there is none.
402 */
403 public final Date getUntil() {
404 return until;
405 }
406
407 /**
408 * @return Returns the weekStartDay or null if there is none.
409 */
410 public final String getWeekStartDay() {
411 return weekStartDay;
412 }
413
414 /**
415 * @param weekStartDay The weekStartDay to set.
416 */
417 public final void setWeekStartDay(final String weekStartDay) {
418 this.weekStartDay = weekStartDay;
419 }
420
421 /**
422 * {@inheritDoc}
423 */
424 public final String toString() {
425 final StringBuffer b = new StringBuffer();
426 b.append(FREQ);
427 b.append('=');
428 b.append(frequency);
429 if (weekStartDay != null) {
430 b.append(';');
431 b.append(WKST);
432 b.append('=');
433 b.append(weekStartDay);
434 }
435 if (until != null) {
436 b.append(';');
437 b.append(UNTIL);
438 b.append('=');
439 // Note: date-time representations should always be in UTC time.
440 b.append(until);
441 }
442 if (count >= 1) {
443 b.append(';');
444 b.append(COUNT);
445 b.append('=');
446 b.append(count);
447 }
448 if (interval >= 1) {
449 b.append(';');
450 b.append(INTERVAL);
451 b.append('=');
452 b.append(interval);
453 }
454 if (!getMonthList().isEmpty()) {
455 b.append(';');
456 b.append(BYMONTH);
457 b.append('=');
458 b.append(monthList);
459 }
460 if (!getWeekNoList().isEmpty()) {
461 b.append(';');
462 b.append(BYWEEKNO);
463 b.append('=');
464 b.append(weekNoList);
465 }
466 if (!getYearDayList().isEmpty()) {
467 b.append(';');
468 b.append(BYYEARDAY);
469 b.append('=');
470 b.append(yearDayList);
471 }
472 if (!getMonthDayList().isEmpty()) {
473 b.append(';');
474 b.append(BYMONTHDAY);
475 b.append('=');
476 b.append(monthDayList);
477 }
478 if (!getDayList().isEmpty()) {
479 b.append(';');
480 b.append(BYDAY);
481 b.append('=');
482 b.append(dayList);
483 }
484 if (!getHourList().isEmpty()) {
485 b.append(';');
486 b.append(BYHOUR);
487 b.append('=');
488 b.append(hourList);
489 }
490 if (!getMinuteList().isEmpty()) {
491 b.append(';');
492 b.append(BYMINUTE);
493 b.append('=');
494 b.append(minuteList);
495 }
496 if (!getSecondList().isEmpty()) {
497 b.append(';');
498 b.append(BYSECOND);
499 b.append('=');
500 b.append(secondList);
501 }
502 if (!getSetPosList().isEmpty()) {
503 b.append(';');
504 b.append(BYSETPOS);
505 b.append('=');
506 b.append(setPosList);
507 }
508 return b.toString();
509 }
510
511 /**
512 * Returns a list of start dates in the specified period represented by this recur. Any date fields not specified by
513 * this recur are retained from the period start, and as such you should ensure the period start is initialised
514 * correctly.
515 * @param periodStart the start of the period
516 * @param periodEnd the end of the period
517 * @param value the type of dates to generate (i.e. date/date-time)
518 * @return a list of dates
519 */
520 public final DateList getDates(final Date periodStart,
521 final Date periodEnd, final Value value) {
522 return getDates(periodStart, periodStart, periodEnd, value, -1);
523 }
524
525 /**
526 * Convenience method for retrieving recurrences in a specified period.
527 * @param seed a seed date for generating recurrence instances
528 * @param period the period of returned recurrence dates
529 * @param value type of dates to generate
530 * @return a list of dates
531 */
532 public final DateList getDates(final Date seed, final Period period,
533 final Value value) {
534 return getDates(seed, period.getStart(), period.getEnd(), value, -1);
535 }
536
537 /**
538 * Returns a list of start dates in the specified period represented by this recur. This method includes a base date
539 * argument, which indicates the start of the fist occurrence of this recurrence. The base date is used to inject
540 * default values to return a set of dates in the correct format. For example, if the search start date (start) is
541 * Wed, Mar 23, 12:19PM, but the recurrence is Mon - Fri, 9:00AM - 5:00PM, the start dates returned should all be at
542 * 9:00AM, and not 12:19PM.
543 * @return a list of dates represented by this recur instance
544 * @param seed the start date of this Recurrence's first instance
545 * @param periodStart the start of the period
546 * @param periodEnd the end of the period
547 * @param value the type of dates to generate (i.e. date/date-time)
548 */
549 public final DateList getDates(final Date seed, final Date periodStart,
550 final Date periodEnd, final Value value) {
551 return getDates(seed, periodStart, periodEnd, value, -1);
552 }
553
554 /**
555 * Returns a list of start dates in the specified period represented by this recur. This method includes a base date
556 * argument, which indicates the start of the fist occurrence of this recurrence. The base date is used to inject
557 * default values to return a set of dates in the correct format. For example, if the search start date (start) is
558 * Wed, Mar 23, 12:19PM, but the recurrence is Mon - Fri, 9:00AM - 5:00PM, the start dates returned should all be at
559 * 9:00AM, and not 12:19PM.
560 * @return a list of dates represented by this recur instance
561 * @param seed the start date of this Recurrence's first instance
562 * @param periodStart the start of the period
563 * @param periodEnd the end of the period
564 * @param value the type of dates to generate (i.e. date/date-time)
565 * @param maxCount limits the number of instances returned. Up to one years
566 * worth extra may be returned. Less than 0 means no limit
567 */
568 public final DateList getDates(final Date seed, final Date periodStart,
569 final Date periodEnd, final Value value,
570 final int maxCount) {
571
572 final DateList dates = new DateList(value);
573 if (seed instanceof DateTime) {
574 if (((DateTime) seed).isUtc()) {
575 dates.setUtc(true);
576 }
577 else {
578 dates.setTimeZone(((DateTime) seed).getTimeZone());
579 }
580 }
581 final Calendar cal = Dates.getCalendarInstance(seed);
582 cal.setTime(seed);
583
584 // optimize the start time for selecting candidates
585 // (only applicable where a COUNT is not specified)
586 if (getCount() < 1) {
587 final Calendar seededCal = (Calendar) cal.clone();
588 while (seededCal.getTime().before(periodStart)) {
589 cal.setTime(seededCal.getTime());
590 increment(seededCal);
591 }
592 }
593
594 int invalidCandidateCount = 0;
595 int noCandidateIncrementCount = 0;
596 Date candidate = null;
597 while ((maxCount < 0) || (dates.size() < maxCount)) {
598 final Date candidateSeed = Dates.getInstance(cal.getTime(), value);
599
600 if (getUntil() != null && candidate != null
601 && candidate.after(getUntil())) {
602
603 break;
604 }
605 if (periodEnd != null && candidate != null
606 && candidate.after(periodEnd)) {
607
608 break;
609 }
610 if (getCount() >= 1
611 && (dates.size() + invalidCandidateCount) >= getCount()) {
612
613 break;
614 }
615
616 // if (Value.DATE_TIME.equals(value)) {
617 if (candidateSeed instanceof DateTime) {
618 if (dates.isUtc()) {
619 ((DateTime) candidateSeed).setUtc(true);
620 }
621 else {
622 ((DateTime) candidateSeed).setTimeZone(dates.getTimeZone());
623 }
624 }
625
626 final DateList candidates = getCandidates(candidateSeed, value);
627 if (!candidates.isEmpty()) {
628 noCandidateIncrementCount = 0;
629 // sort candidates for identifying when UNTIL date is exceeded..
630 Collections.sort(candidates);
631 for (final Iterator i = candidates.iterator(); i.hasNext();) {
632 candidate = (Date) i.next();
633 // don't count candidates that occur before the seed date..
634 if (!candidate.before(seed)) {
635 // candidates exclusive of periodEnd..
636 if (candidate.before(periodStart)
637 || !candidate.before(periodEnd)) {
638 invalidCandidateCount++;
639 } else if (getCount() >= 1
640 && (dates.size() + invalidCandidateCount) >= getCount()) {
641 break;
642 } else if (!(getUntil() != null
643 && candidate.after(getUntil()))) {
644 dates.add(candidate);
645 }
646 }
647 }
648 } else {
649 noCandidateIncrementCount++;
650 if ((maxIncrementCount > 0) && (noCandidateIncrementCount > maxIncrementCount)) {
651 break;
652 }
653 }
654 increment(cal);
655 }
656 // sort final list..
657 Collections.sort(dates);
658 return dates;
659 }
660
661 /**
662 * Returns the the next date of this recurrence given a seed date
663 * and start date. The seed date indicates the start of the fist
664 * occurrence of this recurrence. The start date is the
665 * starting date to search for the next recurrence. Return null
666 * if there is no occurrence date after start date.
667 * @return the next date in the recurrence series after startDate
668 * @param seed the start date of this Recurrence's first instance
669 * @param startDate the date to start the search
670 */
671 public final Date getNextDate(final Date seed, final Date startDate) {
672
673 final Calendar cal = Dates.getCalendarInstance(seed);
674 cal.setTime(seed);
675
676 // optimize the start time for selecting candidates
677 // (only applicable where a COUNT is not specified)
678 if (getCount() < 1) {
679 final Calendar seededCal = (Calendar) cal.clone();
680 while (seededCal.getTime().before(startDate)) {
681 cal.setTime(seededCal.getTime());
682 increment(seededCal);
683 }
684 }
685
686 int invalidCandidateCount = 0;
687 int noCandidateIncrementCount = 0;
688 Date candidate = null;
689 final Value value = seed instanceof DateTime ? Value.DATE_TIME : Value.DATE;
690
691 while (true) {
692 final Date candidateSeed = Dates.getInstance(cal.getTime(), value);
693
694 if (getUntil() != null && candidate != null && candidate.after(getUntil())) {
695 break;
696 }
697
698 if (getCount() > 0 && invalidCandidateCount >= getCount()) {
699 break;
700 }
701
702 if (Value.DATE_TIME.equals(value)) {
703 if (((DateTime) seed).isUtc()) {
704 ((DateTime) candidateSeed).setUtc(true);
705 }
706 else {
707 ((DateTime) candidateSeed).setTimeZone(((DateTime) seed).getTimeZone());
708 }
709 }
710
711 final DateList candidates = getCandidates(candidateSeed, value);
712 if (!candidates.isEmpty()) {
713 noCandidateIncrementCount = 0;
714 // sort candidates for identifying when UNTIL date is exceeded..
715 Collections.sort(candidates);
716
717 for (final Iterator i = candidates.iterator(); i.hasNext();) {
718 candidate = (Date) i.next();
719 // don't count candidates that occur before the seed date..
720 if (!candidate.before(seed)) {
721 // Candidate must be after startDate because
722 // we want the NEXT occurrence
723 if (!candidate.after(startDate)) {
724 invalidCandidateCount++;
725 } else if (getCount() > 0
726 && invalidCandidateCount >= getCount()) {
727 break;
728 } else if (!(getUntil() != null
729 && candidate.after(getUntil()))) {
730 return candidate;
731 }
732 }
733 }
734 } else {
735 noCandidateIncrementCount++;
736 if ((maxIncrementCount > 0) && (noCandidateIncrementCount > maxIncrementCount)) {
737 break;
738 }
739 }
740 increment(cal);
741 }
742 return null;
743 }
744
745 /**
746 * Increments the specified calendar according to the frequency and interval specified in this recurrence rule.
747 * @param cal a java.util.Calendar to increment
748 */
749 private void increment(final Calendar cal) {
750 // initialise interval..
751 final int calInterval = (getInterval() >= 1) ? getInterval() : 1;
752 cal.add(calIncField, calInterval);
753 }
754
755 /**
756 * Returns a list of possible dates generated from the applicable BY* rules, using the specified date as a seed.
757 * @param date the seed date
758 * @param value the type of date list to return
759 * @return a DateList
760 */
761 private DateList getCandidates(final Date date, final Value value) {
762 DateList dates = new DateList(value);
763 if (date instanceof DateTime) {
764 if (((DateTime) date).isUtc()) {
765 dates.setUtc(true);
766 }
767 else {
768 dates.setTimeZone(((DateTime) date).getTimeZone());
769 }
770 }
771 dates.add(date);
772 dates = getMonthVariants(dates);
773 // debugging..
774 if (log.isDebugEnabled()) {
775 log.debug("Dates after BYMONTH processing: " + dates);
776 }
777 dates = getWeekNoVariants(dates);
778 // debugging..
779 if (log.isDebugEnabled()) {
780 log.debug("Dates after BYWEEKNO processing: " + dates);
781 }
782 dates = getYearDayVariants(dates);
783 // debugging..
784 if (log.isDebugEnabled()) {
785 log.debug("Dates after BYYEARDAY processing: " + dates);
786 }
787 dates = getMonthDayVariants(dates);
788 // debugging..
789 if (log.isDebugEnabled()) {
790 log.debug("Dates after BYMONTHDAY processing: " + dates);
791 }
792 dates = getDayVariants(dates);
793 // debugging..
794 if (log.isDebugEnabled()) {
795 log.debug("Dates after BYDAY processing: " + dates);
796 }
797 dates = getHourVariants(dates);
798 // debugging..
799 if (log.isDebugEnabled()) {
800 log.debug("Dates after BYHOUR processing: " + dates);
801 }
802 dates = getMinuteVariants(dates);
803 // debugging..
804 if (log.isDebugEnabled()) {
805 log.debug("Dates after BYMINUTE processing: " + dates);
806 }
807 dates = getSecondVariants(dates);
808 // debugging..
809 if (log.isDebugEnabled()) {
810 log.debug("Dates after BYSECOND processing: " + dates);
811 }
812 dates = applySetPosRules(dates);
813 // debugging..
814 if (log.isDebugEnabled()) {
815 log.debug("Dates after SETPOS processing: " + dates);
816 }
817 return dates;
818 }
819
820 /**
821 * Applies BYSETPOS rules to <code>dates</code>. Valid positions are from 1 to the size of the date list. Invalid
822 * positions are ignored.
823 * @param dates
824 */
825 private DateList applySetPosRules(final DateList dates) {
826 // return if no SETPOS rules specified..
827 if (getSetPosList().isEmpty()) {
828 return dates;
829 }
830 // sort the list before processing..
831 Collections.sort(dates);
832 final DateList setPosDates = getDateListInstance(dates);
833 final int size = dates.size();
834 for (final Iterator i = getSetPosList().iterator(); i.hasNext();) {
835 final Integer setPos = (Integer) i.next();
836 final int pos = setPos.intValue();
837 if (pos > 0 && pos <= size) {
838 setPosDates.add(dates.get(pos - 1));
839 }
840 else if (pos < 0 && pos >= -size) {
841 setPosDates.add(dates.get(size + pos));
842 }
843 }
844 return setPosDates;
845 }
846
847 /**
848 * Applies BYMONTH rules specified in this Recur instance to the specified date list. If no BYMONTH rules are
849 * specified the date list is returned unmodified.
850 * @param dates
851 * @return
852 */
853 private DateList getMonthVariants(final DateList dates) {
854 if (getMonthList().isEmpty()) {
855 return dates;
856 }
857 final DateList monthlyDates = getDateListInstance(dates);
858 for (final Iterator i = dates.iterator(); i.hasNext();) {
859 final Date date = (Date) i.next();
860 final Calendar cal = Dates.getCalendarInstance(date);
861 cal.setTime(date);
862 for (final Iterator j = getMonthList().iterator(); j.hasNext();) {
863 final Integer month = (Integer) j.next();
864 // Java months are zero-based..
865 // cal.set(Calendar.MONTH, month.intValue() - 1);
866 cal.roll(Calendar.MONTH, (month.intValue() - 1) - cal.get(Calendar.MONTH));
867 monthlyDates.add(Dates.getInstance(cal.getTime(), monthlyDates.getType()));
868 }
869 }
870 return monthlyDates;
871 }
872
873 /**
874 * Applies BYWEEKNO rules specified in this Recur instance to the specified date list. If no BYWEEKNO rules are
875 * specified the date list is returned unmodified.
876 * @param dates
877 * @return
878 */
879 private DateList getWeekNoVariants(final DateList dates) {
880 if (getWeekNoList().isEmpty()) {
881 return dates;
882 }
883 final DateList weekNoDates = getDateListInstance(dates);
884 for (final Iterator i = dates.iterator(); i.hasNext();) {
885 final Date date = (Date) i.next();
886 final Calendar cal = Dates.getCalendarInstance(date);
887 cal.setTime(date);
888 for (final Iterator j = getWeekNoList().iterator(); j.hasNext();) {
889 final Integer weekNo = (Integer) j.next();
890 cal.set(Calendar.WEEK_OF_YEAR, Dates.getAbsWeekNo(cal.getTime(), weekNo.intValue()));
891 weekNoDates.add(Dates.getInstance(cal.getTime(), weekNoDates.getType()));
892 }
893 }
894 return weekNoDates;
895 }
896
897 /**
898 * Applies BYYEARDAY rules specified in this Recur instance to the specified date list. If no BYYEARDAY rules are
899 * specified the date list is returned unmodified.
900 * @param dates
901 * @return
902 */
903 private DateList getYearDayVariants(final DateList dates) {
904 if (getYearDayList().isEmpty()) {
905 return dates;
906 }
907 final DateList yearDayDates = getDateListInstance(dates);
908 for (final Iterator i = dates.iterator(); i.hasNext();) {
909 final Date date = (Date) i.next();
910 final Calendar cal = Dates.getCalendarInstance(date);
911 cal.setTime(date);
912 for (final Iterator j = getYearDayList().iterator(); j.hasNext();) {
913 final Integer yearDay = (Integer) j.next();
914 cal.set(Calendar.DAY_OF_YEAR, Dates.getAbsYearDay(cal.getTime(), yearDay.intValue()));
915 yearDayDates.add(Dates.getInstance(cal.getTime(), yearDayDates.getType()));
916 }
917 }
918 return yearDayDates;
919 }
920
921 /**
922 * Applies BYMONTHDAY rules specified in this Recur instance to the specified date list. If no BYMONTHDAY rules are
923 * specified the date list is returned unmodified.
924 * @param dates
925 * @return
926 */
927 private DateList getMonthDayVariants(final DateList dates) {
928 if (getMonthDayList().isEmpty()) {
929 return dates;
930 }
931 final DateList monthDayDates = getDateListInstance(dates);
932 for (final Iterator i = dates.iterator(); i.hasNext();) {
933 final Date date = (Date) i.next();
934 final Calendar cal = Dates.getCalendarInstance(date);
935 cal.setLenient(false);
936 cal.setTime(date);
937 for (final Iterator j = getMonthDayList().iterator(); j.hasNext();) {
938 final Integer monthDay = (Integer) j.next();
939 try {
940 cal.set(Calendar.DAY_OF_MONTH, Dates.getAbsMonthDay(cal.getTime(), monthDay.intValue()));
941 monthDayDates.add(Dates.getInstance(cal.getTime(), monthDayDates.getType()));
942 }
943 catch (IllegalArgumentException iae) {
944 if (log.isTraceEnabled()) {
945 log.trace("Invalid day of month: " + Dates.getAbsMonthDay(cal
946 .getTime(), monthDay.intValue()));
947 }
948 }
949 }
950 }
951 return monthDayDates;
952 }
953
954 /**
955 * Applies BYDAY rules specified in this Recur instance to the specified date list. If no BYDAY rules are specified
956 * the date list is returned unmodified.
957 * @param dates
958 * @return
959 */
960 private DateList getDayVariants(final DateList dates) {
961 if (getDayList().isEmpty()) {
962 return dates;
963 }
964 final DateList weekDayDates = getDateListInstance(dates);
965 for (final Iterator i = dates.iterator(); i.hasNext();) {
966 final Date date = (Date) i.next();
967 for (final Iterator j = getDayList().iterator(); j.hasNext();) {
968 final WeekDay weekDay = (WeekDay) j.next();
969 // if BYYEARDAY or BYMONTHDAY is specified filter existing
970 // list..
971 if (!getYearDayList().isEmpty() || !getMonthDayList().isEmpty()) {
972 final Calendar cal = Dates.getCalendarInstance(date);
973 cal.setTime(date);
974 if (weekDay.equals(WeekDay.getWeekDay(cal))) {
975 weekDayDates.add(date);
976 }
977 }
978 else {
979 weekDayDates.addAll(getAbsWeekDays(date, dates.getType(), weekDay));
980 }
981 }
982 }
983 return weekDayDates;
984 }
985
986 /**
987 * Returns a list of applicable dates corresponding to the specified week day in accordance with the frequency
988 * specified by this recurrence rule.
989 * @param date
990 * @param weekDay
991 * @return
992 */
993 private List getAbsWeekDays(final Date date, final Value type, final WeekDay weekDay) {
994 final Calendar cal = Dates.getCalendarInstance(date);
995 // default week start is Monday per RFC5545
996 int calendarWeekStartDay = Calendar.MONDAY;
997 if (weekStartDay != null) {
998 calendarWeekStartDay = WeekDay.getCalendarDay(new WeekDay(weekStartDay));
999 }
1000 cal.setFirstDayOfWeek(calendarWeekStartDay);
1001 cal.setTime(date);
1002
1003 final DateList days = new DateList(type);
1004 if (date instanceof DateTime) {
1005 if (((DateTime) date).isUtc()) {
1006 days.setUtc(true);
1007 }
1008 else {
1009 days.setTimeZone(((DateTime) date).getTimeZone());
1010 }
1011 }
1012 final int calDay = WeekDay.getCalendarDay(weekDay);
1013 if (calDay == -1) {
1014 // a matching weekday cannot be identified..
1015 return days;
1016 }
1017 if (DAILY.equals(getFrequency())) {
1018 if (cal.get(Calendar.DAY_OF_WEEK) == calDay) {
1019 days.add(Dates.getInstance(cal.getTime(), type));
1020 }
1021 }
1022 else if (WEEKLY.equals(getFrequency()) || !getWeekNoList().isEmpty()) {
1023 final int weekNo = cal.get(Calendar.WEEK_OF_YEAR);
1024 // construct a list of possible week days..
1025 cal.set(Calendar.DAY_OF_WEEK, cal.getFirstDayOfWeek());
1026 while (cal.get(Calendar.DAY_OF_WEEK) != calDay) {
1027 cal.add(Calendar.DAY_OF_WEEK, 1);
1028 }
1029 // final int weekNo = cal.get(Calendar.WEEK_OF_YEAR);
1030 if (cal.get(Calendar.WEEK_OF_YEAR) == weekNo) {
1031 days.add(Dates.getInstance(cal.getTime(), type));
1032 // cal.add(Calendar.DAY_OF_WEEK, Dates.DAYS_PER_WEEK);
1033 }
1034 }
1035 else if (MONTHLY.equals(getFrequency()) || !getMonthList().isEmpty()) {
1036 final int month = cal.get(Calendar.MONTH);
1037 // construct a list of possible month days..
1038 cal.set(Calendar.DAY_OF_MONTH, 1);
1039 while (cal.get(Calendar.DAY_OF_WEEK) != calDay) {
1040 cal.add(Calendar.DAY_OF_MONTH, 1);
1041 }
1042 while (cal.get(Calendar.MONTH) == month) {
1043 days.add(Dates.getInstance(cal.getTime(), type));
1044 cal.add(Calendar.DAY_OF_MONTH, Dates.DAYS_PER_WEEK);
1045 }
1046 }
1047 else if (YEARLY.equals(getFrequency())) {
1048 final int year = cal.get(Calendar.YEAR);
1049 // construct a list of possible year days..
1050 cal.set(Calendar.DAY_OF_YEAR, 1);
1051 while (cal.get(Calendar.DAY_OF_WEEK) != calDay) {
1052 cal.add(Calendar.DAY_OF_YEAR, 1);
1053 }
1054 while (cal.get(Calendar.YEAR) == year) {
1055 days.add(Dates.getInstance(cal.getTime(), type));
1056 cal.add(Calendar.DAY_OF_YEAR, Dates.DAYS_PER_WEEK);
1057 }
1058 }
1059 return getOffsetDates(days, weekDay.getOffset());
1060 }
1061
1062 /**
1063 * Returns a single-element sublist containing the element of <code>list</code> at <code>offset</code>. Valid
1064 * offsets are from 1 to the size of the list. If an invalid offset is supplied, all elements from <code>list</code>
1065 * are added to <code>sublist</code>.
1066 * @param list
1067 * @param offset
1068 * @param sublist
1069 */
1070 private List getOffsetDates(final DateList dates, final int offset) {
1071 if (offset == 0) {
1072 return dates;
1073 }
1074 final List offsetDates = getDateListInstance(dates);
1075 final int size = dates.size();
1076 if (offset < 0 && offset >= -size) {
1077 offsetDates.add(dates.get(size + offset));
1078 }
1079 else if (offset > 0 && offset <= size) {
1080 offsetDates.add(dates.get(offset - 1));
1081 }
1082 return offsetDates;
1083 }
1084
1085 /**
1086 * Applies BYHOUR rules specified in this Recur instance to the specified date list. If no BYHOUR rules are
1087 * specified the date list is returned unmodified.
1088 * @param dates
1089 * @return
1090 */
1091 private DateList getHourVariants(final DateList dates) {
1092 if (getHourList().isEmpty()) {
1093 return dates;
1094 }
1095 final DateList hourlyDates = getDateListInstance(dates);
1096 for (final Iterator i = dates.iterator(); i.hasNext();) {
1097 final Date date = (Date) i.next();
1098 final Calendar cal = Dates.getCalendarInstance(date);
1099 cal.setTime(date);
1100 for (final Iterator j = getHourList().iterator(); j.hasNext();) {
1101 final Integer hour = (Integer) j.next();
1102 cal.set(Calendar.HOUR_OF_DAY, hour.intValue());
1103 hourlyDates.add(Dates.getInstance(cal.getTime(), hourlyDates.getType()));
1104 }
1105 }
1106 return hourlyDates;
1107 }
1108
1109 /**
1110 * Applies BYMINUTE rules specified in this Recur instance to the specified date list. If no BYMINUTE rules are
1111 * specified the date list is returned unmodified.
1112 * @param dates
1113 * @return
1114 */
1115 private DateList getMinuteVariants(final DateList dates) {
1116 if (getMinuteList().isEmpty()) {
1117 return dates;
1118 }
1119 final DateList minutelyDates = getDateListInstance(dates);
1120 for (final Iterator i = dates.iterator(); i.hasNext();) {
1121 final Date date = (Date) i.next();
1122 final Calendar cal = Dates.getCalendarInstance(date);
1123 cal.setTime(date);
1124 for (final Iterator j = getMinuteList().iterator(); j.hasNext();) {
1125 final Integer minute = (Integer) j.next();
1126 cal.set(Calendar.MINUTE, minute.intValue());
1127 minutelyDates.add(Dates.getInstance(cal.getTime(), minutelyDates.getType()));
1128 }
1129 }
1130 return minutelyDates;
1131 }
1132
1133 /**
1134 * Applies BYSECOND rules specified in this Recur instance to the specified date list. If no BYSECOND rules are
1135 * specified the date list is returned unmodified.
1136 * @param dates
1137 * @return
1138 */
1139 private DateList getSecondVariants(final DateList dates) {
1140 if (getSecondList().isEmpty()) {
1141 return dates;
1142 }
1143 final DateList secondlyDates = getDateListInstance(dates);
1144 for (final Iterator i = dates.iterator(); i.hasNext();) {
1145 final Date date = (Date) i.next();
1146 final Calendar cal = Dates.getCalendarInstance(date);
1147 cal.setTime(date);
1148 for (final Iterator j = getSecondList().iterator(); j.hasNext();) {
1149 final Integer second = (Integer) j.next();
1150 cal.set(Calendar.SECOND, second.intValue());
1151 secondlyDates.add(Dates.getInstance(cal.getTime(), secondlyDates.getType()));
1152 }
1153 }
1154 return secondlyDates;
1155 }
1156
1157 private void validateFrequency() {
1158 if (frequency == null) {
1159 throw new IllegalArgumentException(
1160 "A recurrence rule MUST contain a FREQ rule part.");
1161 }
1162 if (SECONDLY.equals(getFrequency())) {
1163 calIncField = Calendar.SECOND;
1164 }
1165 else if (MINUTELY.equals(getFrequency())) {
1166 calIncField = Calendar.MINUTE;
1167 }
1168 else if (HOURLY.equals(getFrequency())) {
1169 calIncField = Calendar.HOUR_OF_DAY;
1170 }
1171 else if (DAILY.equals(getFrequency())) {
1172 calIncField = Calendar.DAY_OF_YEAR;
1173 }
1174 else if (WEEKLY.equals(getFrequency())) {
1175 calIncField = Calendar.WEEK_OF_YEAR;
1176 }
1177 else if (MONTHLY.equals(getFrequency())) {
1178 calIncField = Calendar.MONTH;
1179 }
1180 else if (YEARLY.equals(getFrequency())) {
1181 calIncField = Calendar.YEAR;
1182 }
1183 else {
1184 throw new IllegalArgumentException("Invalid FREQ rule part '"
1185 + frequency + "' in recurrence rule");
1186 }
1187 }
1188
1189 /**
1190 * @param count The count to set.
1191 */
1192 public final void setCount(final int count) {
1193 this.count = count;
1194 this.until = null;
1195 }
1196
1197 /**
1198 * @param frequency The frequency to set.
1199 */
1200 public final void setFrequency(final String frequency) {
1201 this.frequency = frequency;
1202 validateFrequency();
1203 }
1204
1205 /**
1206 * @param interval The interval to set.
1207 */
1208 public final void setInterval(final int interval) {
1209 this.interval = interval;
1210 }
1211
1212 /**
1213 * @param until The until to set.
1214 */
1215 public final void setUntil(final Date until) {
1216 this.until = until;
1217 this.count = -1;
1218 }
1219
1220 /**
1221 * @param stream
1222 * @throws IOException
1223 * @throws ClassNotFoundException
1224 */
1225 private void readObject(final java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException {
1226 stream.defaultReadObject();
1227 log = LogFactory.getLog(Recur.class);
1228 }
1229
1230 /**
1231 * Instantiate a new datelist with the same type, timezone and utc settings
1232 * as the origList.
1233 * @param origList
1234 * @return a new empty list.
1235 */
1236 private static final DateList getDateListInstance(final DateList origList) {
1237 final DateList list = new DateList(origList.getType());
1238 if (origList.isUtc()) {
1239 list.setUtc(true);
1240 } else {
1241 list.setTimeZone(origList.getTimeZone());
1242 }
1243 return list;
1244 }
1245
1246 }

mercurial