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

Tue, 10 Feb 2015 18:12:00 +0100

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

Import initial revisions of existing project AndroidCaldavSyncAdapater,
forked from upstream repository at 27e8a0f8495c92e0780d450bdf0c7cec77a03a55.

     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.Configurator;
    48 import net.fortuna.ical4j.util.Dates;
    50 import org.apache.commons.logging.Log;
    51 import org.apache.commons.logging.LogFactory;
    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 {
    62     private static final long serialVersionUID = -7333226591784095142L;
    64     private static final String FREQ = "FREQ";
    66     private static final String UNTIL = "UNTIL";
    68     private static final String COUNT = "COUNT";
    70     private static final String INTERVAL = "INTERVAL";
    72     private static final String BYSECOND = "BYSECOND";
    74     private static final String BYMINUTE = "BYMINUTE";
    76     private static final String BYHOUR = "BYHOUR";
    78     private static final String BYDAY = "BYDAY";
    80     private static final String BYMONTHDAY = "BYMONTHDAY";
    82     private static final String BYYEARDAY = "BYYEARDAY";
    84     private static final String BYWEEKNO = "BYWEEKNO";
    86     private static final String BYMONTH = "BYMONTH";
    88     private static final String BYSETPOS = "BYSETPOS";
    90     private static final String WKST = "WKST";
    92     /**
    93      * Second frequency resolution.
    94      */
    95     public static final String SECONDLY = "SECONDLY";
    97     /**
    98      * Minute frequency resolution.
    99      */
   100     public static final String MINUTELY = "MINUTELY";
   102     /**
   103      * Hour frequency resolution.
   104      */
   105     public static final String HOURLY = "HOURLY";
   107     /**
   108      * Day frequency resolution.
   109      */
   110     public static final String DAILY = "DAILY";
   112     /**
   113      * Week frequency resolution.
   114      */
   115     public static final String WEEKLY = "WEEKLY";
   117     /**
   118      * Month frequency resolution.
   119      */
   120     public static final String MONTHLY = "MONTHLY";
   122     /**
   123      * Year frequency resolution.
   124      */
   125     public static final String YEARLY = "YEARLY";
   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";
   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     }
   145     private transient Log log = LogFactory.getLog(Recur.class);
   147     private String frequency;
   149     private Date until;
   151     private int count = -1;
   153     private int interval = -1;
   155     private NumberList secondList;
   157     private NumberList minuteList;
   159     private NumberList hourList;
   161     private WeekDayList dayList;
   163     private NumberList monthDayList;
   165     private NumberList yearDayList;
   167     private NumberList weekNoList;
   169     private NumberList monthList;
   171     private NumberList setPosList;
   173     private String weekStartDay;
   175     private Map experimentalValues = new HashMap();
   177     // Calendar field we increment based on frequency.
   178     private int calIncField;
   180     /**
   181      * Default constructor.
   182      */
   183     public Recur() {
   184     }
   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     }
   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     }
   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     }
   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     }
   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     }
   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     }
   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     }
   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     }
   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     }
   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     }
   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     }
   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     }
   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     }
   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     }
   379     /**
   380      * @return Returns the experimentalValues.
   381      */
   382     public final Map getExperimentalValues() {
   383         return experimentalValues;
   384     }
   386     /**
   387      * @return Returns the frequency.
   388      */
   389     public final String getFrequency() {
   390         return frequency;
   391     }
   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     }
   400     /**
   401      * @return Returns the until or null if there is none.
   402      */
   403     public final Date getUntil() {
   404         return until;
   405     }
   407     /**
   408      * @return Returns the weekStartDay or null if there is none.
   409      */
   410     public final String getWeekStartDay() {
   411         return weekStartDay;
   412     }
   414     /**
   415      * @param weekStartDay The weekStartDay to set.
   416      */
   417     public final void setWeekStartDay(final String weekStartDay) {
   418         this.weekStartDay = weekStartDay;
   419     }
   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     }
   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     }
   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     }
   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     }
   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) {
   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);
   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         }
   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);
   600             if (getUntil() != null && candidate != null
   601                     && candidate.after(getUntil())) {
   603                 break;
   604             }
   605             if (periodEnd != null && candidate != null
   606                     && candidate.after(periodEnd)) {
   608                 break;
   609             }
   610             if (getCount() >= 1
   611                     && (dates.size() + invalidCandidateCount) >= getCount()) {
   613                 break;
   614             }
   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             }
   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     }
   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) {
   673         final Calendar cal = Dates.getCalendarInstance(seed);
   674         cal.setTime(seed);
   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         }
   686         int invalidCandidateCount = 0;
   687         int noCandidateIncrementCount = 0;
   688         Date candidate = null;
   689         final Value value = seed instanceof DateTime ? Value.DATE_TIME : Value.DATE;
   691         while (true) {
   692             final Date candidateSeed = Dates.getInstance(cal.getTime(), value);
   694             if (getUntil() != null && candidate != null && candidate.after(getUntil())) {
   695                 break;
   696             }
   698             if (getCount() > 0 && invalidCandidateCount >= getCount()) {
   699                 break;
   700             }
   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             }
   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);
   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     }
   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     }
   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     }
   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     }
   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     }
   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     }
   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     }
   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     }
   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     }
   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);
  1003         final DateList days = new DateList(type);
  1004         if (date instanceof DateTime) {
  1005             if (((DateTime) date).isUtc()) {
  1006                 days.setUtc(true);
  1008             else {
  1009                 days.setTimeZone(((DateTime) date).getTimeZone());
  1012         final int calDay = WeekDay.getCalendarDay(weekDay);
  1013         if (calDay == -1) {
  1014             // a matching weekday cannot be identified..
  1015             return days;
  1017         if (DAILY.equals(getFrequency())) {
  1018             if (cal.get(Calendar.DAY_OF_WEEK) == calDay) {
  1019                 days.add(Dates.getInstance(cal.getTime(), type));
  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);
  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);
  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);
  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);
  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);
  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);
  1059         return getOffsetDates(days, weekDay.getOffset());
  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;
  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));
  1079         else if (offset > 0 && offset <= size) {
  1080             offsetDates.add(dates.get(offset - 1));
  1082         return offsetDates;
  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;
  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()));
  1106         return hourlyDates;
  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;
  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()));
  1130         return minutelyDates;
  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;
  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()));
  1154         return secondlyDates;
  1157     private void validateFrequency() {
  1158         if (frequency == null) {
  1159             throw new IllegalArgumentException(
  1160                     "A recurrence rule MUST contain a FREQ rule part.");
  1162         if (SECONDLY.equals(getFrequency())) {
  1163             calIncField = Calendar.SECOND;
  1165         else if (MINUTELY.equals(getFrequency())) {
  1166             calIncField = Calendar.MINUTE;
  1168         else if (HOURLY.equals(getFrequency())) {
  1169             calIncField = Calendar.HOUR_OF_DAY;
  1171         else if (DAILY.equals(getFrequency())) {
  1172             calIncField = Calendar.DAY_OF_YEAR;
  1174         else if (WEEKLY.equals(getFrequency())) {
  1175             calIncField = Calendar.WEEK_OF_YEAR;
  1177         else if (MONTHLY.equals(getFrequency())) {
  1178             calIncField = Calendar.MONTH;
  1180         else if (YEARLY.equals(getFrequency())) {
  1181             calIncField = Calendar.YEAR;
  1183         else {
  1184             throw new IllegalArgumentException("Invalid FREQ rule part '"
  1185                     + frequency + "' in recurrence rule");
  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;
  1197     /**
  1198      * @param frequency The frequency to set.
  1199      */
  1200     public final void setFrequency(final String frequency) {
  1201         this.frequency = frequency;
  1202         validateFrequency();
  1205     /**
  1206      * @param interval The interval to set.
  1207      */
  1208     public final void setInterval(final int interval) {
  1209         this.interval = interval;
  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;
  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);
  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());
  1243         return list;

mercurial