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

Tue, 10 Feb 2015 19:38:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 10 Feb 2015 19:38:00 +0100
changeset 3
73bdfa70b04e
parent 0
fb9019fb1bf7
permissions
-rw-r--r--

Upgrade embedded ical4j from ancient whatever to upstream version 1.0.6.

     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);
  1014             else {
  1015                 days.setTimeZone(((DateTime) date).getTimeZone());
  1018         final int calDay = WeekDay.getCalendarDay(weekDay);
  1019         if (calDay == -1) {
  1020             // a matching weekday cannot be identified..
  1021             return days;
  1023         if (DAILY.equals(getFrequency())) {
  1024             if (cal.get(Calendar.DAY_OF_WEEK) == calDay) {
  1025                 days.add(Dates.getInstance(cal.getTime(), type));
  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);
  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);
  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);
  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);
  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);
  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);
  1065         return getOffsetDates(days, weekDay.getOffset());
  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;
  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));
  1085         else if (offset > 0 && offset <= size) {
  1086             offsetDates.add(dates.get(offset - 1));
  1088         return offsetDates;
  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;
  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()));
  1111         return hourlyDates;
  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;
  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()));
  1134         return minutelyDates;
  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;
  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()));
  1157         return secondlyDates;
  1160     private void validateFrequency() {
  1161         if (frequency == null) {
  1162             throw new IllegalArgumentException(
  1163                     "A recurrence rule MUST contain a FREQ rule part.");
  1165         if (SECONDLY.equals(getFrequency())) {
  1166             calIncField = Calendar.SECOND;
  1168         else if (MINUTELY.equals(getFrequency())) {
  1169             calIncField = Calendar.MINUTE;
  1171         else if (HOURLY.equals(getFrequency())) {
  1172             calIncField = Calendar.HOUR_OF_DAY;
  1174         else if (DAILY.equals(getFrequency())) {
  1175             calIncField = Calendar.DAY_OF_YEAR;
  1177         else if (WEEKLY.equals(getFrequency())) {
  1178             calIncField = Calendar.WEEK_OF_YEAR;
  1180         else if (MONTHLY.equals(getFrequency())) {
  1181             calIncField = Calendar.MONTH;
  1183         else if (YEARLY.equals(getFrequency())) {
  1184             calIncField = Calendar.YEAR;
  1186         else {
  1187             throw new IllegalArgumentException("Invalid FREQ rule part '"
  1188                     + frequency + "' in recurrence rule");
  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;
  1200     /**
  1201      * @param frequency The frequency to set.
  1202      */
  1203     public final void setFrequency(final String frequency) {
  1204         this.frequency = frequency;
  1205         validateFrequency();
  1208     /**
  1209      * @param interval The interval to set.
  1210      */
  1211     public final void setInterval(final int interval) {
  1212         this.interval = interval;
  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;
  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;
  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);
  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());
  1263         return list;

mercurial