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