Java tutorial
/* * ***** BEGIN LICENSE BLOCK ***** * Zimbra Collaboration Suite Server * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2013, 2014, 2016 Synacor, Inc. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software Foundation, * version 2 of the License. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * You should have received a copy of the GNU General Public License along with this program. * If not, see <https://www.gnu.org/licenses/>. * ***** END LICENSE BLOCK ***** */ package com.zimbra.cs.mailbox.calendar; import java.text.ParseException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.Formatter; import java.util.GregorianCalendar; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import org.apache.commons.httpclient.util.DateUtil; import com.zimbra.common.calendar.ICalTimeZone; import com.zimbra.common.calendar.ParsedDateTime; import com.zimbra.common.calendar.TimeZoneMap; import com.zimbra.common.calendar.ZWeekDay; import com.zimbra.common.service.ServiceException; import com.zimbra.common.util.ZimbraLog; import com.zimbra.cs.account.Provisioning; import com.zimbra.cs.account.Server; import com.zimbra.cs.util.Zimbra; public class ZRecur implements Cloneable { public static enum Frequency { DAILY, HOURLY, MINUTELY, MONTHLY, SECONDLY, WEEKLY, YEARLY } public static class ZWeekDayNum { public static class DayOnlyComparator implements Comparator<ZWeekDayNum> { @Override public int compare(ZWeekDayNum lhs, ZWeekDayNum rhs) { return lhs.mDay.getCalendarDay() - rhs.mDay.getCalendarDay(); } } public ZWeekDay mDay; public int mOrdinal; // -4,-3,-2,-1,+1,+2,+3,+4 public ZWeekDayNum() { } public ZWeekDayNum(int ord, ZWeekDay day) { mOrdinal = ord; mDay = day; }; @Override public String toString() { if (mOrdinal != 0) return Integer.toString(mOrdinal) + mDay; else return mDay.toString(); } }; private static enum Tokens { BYDAY, BYHOUR, BYMINUTE, BYMONTH, BYMONTHDAY, BYSECOND, BYSETPOS, BYWEEKNO, BYYEARDAY, COUNT, FREQ, INTERVAL, UNTIL, WKST; } // Max date known to calendaring = 01/01/9000 00:00:00 in UTC // This value is comfortably smaller than MySQL DATETIME column's max value of 9999-12-31 23:59:59 private static final long MAX_DATE_MILLIS = 221845392000000L; // hard limits to recurrence expansion private static class ExpansionLimits { public int maxInstances; public int maxDays; public int maxWeeks; public int maxMonths; public int maxYears; public int maxYearsOtherFreqs; } private static ExpansionLimits sExpansionLimits; static { sExpansionLimits = new ExpansionLimits(); try { Server server = Provisioning.getInstance().getLocalServer(); String val; val = server.getAttr(Provisioning.A_zimbraCalendarRecurrenceMaxInstances); sExpansionLimits.maxInstances = val == null ? 0 : Integer.parseInt(val); val = server.getAttr(Provisioning.A_zimbraCalendarRecurrenceDailyMaxDays); sExpansionLimits.maxDays = val == null ? 730 : Integer.parseInt(val); val = server.getAttr(Provisioning.A_zimbraCalendarRecurrenceWeeklyMaxWeeks); sExpansionLimits.maxWeeks = val == null ? 520 : Integer.parseInt(val); val = server.getAttr(Provisioning.A_zimbraCalendarRecurrenceMonthlyMaxMonths); sExpansionLimits.maxMonths = val == null ? 360 : Integer.parseInt(val); val = server.getAttr(Provisioning.A_zimbraCalendarRecurrenceYearlyMaxYears); sExpansionLimits.maxYears = val == null ? 100 : Integer.parseInt(val); val = server.getAttr(Provisioning.A_zimbraCalendarRecurrenceOtherFrequencyMaxYears); sExpansionLimits.maxYearsOtherFreqs = val == null ? 1 : Integer.parseInt(val); } catch (NumberFormatException e) { Zimbra.halt("Can't initialize recurrence expansion limits", e); } catch (ServiceException e) { Zimbra.halt("Can't initialize recurrence expansion limits", e); } } private Date estimateEndTimeByUntilAndHardLimits(ParsedDateTime dtStart) throws ServiceException { boolean forever = false; Calendar hardEnd = dtStart.getCalendarCopy(); Frequency freq = mFreq; if (freq == null) freq = Frequency.WEEKLY; switch (freq) { case WEEKLY: int weeks = sExpansionLimits.maxWeeks; if (weeks <= 0) forever = true; else hardEnd.add(Calendar.WEEK_OF_YEAR, weeks); break; case MONTHLY: int months = sExpansionLimits.maxMonths; if (months <= 0) forever = true; else hardEnd.add(Calendar.MONTH, months); break; case YEARLY: int years = sExpansionLimits.maxYears; if (years <= 0) forever = true; else hardEnd.add(Calendar.YEAR, years); break; case DAILY: int days = sExpansionLimits.maxDays; if (days <= 0) forever = true; else hardEnd.add(Calendar.DAY_OF_YEAR, days); break; default: int otherFreqYears = Math.max(sExpansionLimits.maxYearsOtherFreqs, 1); hardEnd.add(Calendar.YEAR, otherFreqYears); } Date d; if (forever) d = new Date(MAX_DATE_MILLIS); else d = hardEnd.getTime(); if (mUntil != null) { Date until = mUntil.getDateForRecurUntil(dtStart.getTimeZone()); if (until.before(d)) d = until; } return d; } public Date getEstimatedEndTime(ParsedDateTime dtStart) throws ServiceException { if (dtStart == null) return null; return estimateEndTimeByUntilAndHardLimits(dtStart); // We can't estimate by COUNT because BYxxx rule parts yield too many possibilities. } public static String listAsStr(List<? extends Object> l) { StringBuffer toRet = new StringBuffer(); boolean first = true; for (Object obj : l) { if (!first) toRet.append(','); else first = false; toRet.append(obj.toString()); } return toRet.toString(); } public static void main(String[] args) { ICalTimeZone tzUTC = ICalTimeZone.getUTC(); TimeZoneMap tzmap = new TimeZoneMap(tzUTC); ParsedDateTime dtStart = null; try { dtStart = ParsedDateTime.parse("20050101T123456", tzmap, tzUTC, tzUTC); } catch (ParseException e) { System.out.println("Caught ParseException at start: " + e); } Date rangeStart; Date rangeEnd; GregorianCalendar cal = new GregorianCalendar(); cal.clear(); cal.setTimeZone(tzUTC); cal.set(2005, 4, 15, 0, 0, 0); rangeStart = cal.getTime(); cal.set(2006, 0, 1, 0, 0, 0); rangeEnd = cal.getTime(); try { ZRecur test = new ZRecur("FREQ=DAILY;BYMONTH=5,6", tzmap); System.out.println("\n\n" + test.toString() + "\n-------------------------------------------------"); List<Date> dateList = test.expandRecurrenceOverRange(dtStart, rangeStart.getTime(), rangeEnd.getTime()); for (Date d : dateList) { cal.setTime(d); cal.setTimeZone(tzUTC); System.out.printf("%tc\n", cal); } } catch (ServiceException e) { System.out.println("Caught ServiceException" + e); e.printStackTrace(); } try { ZRecur test = new ZRecur("FREQ=DAILY;BYMONTH=5,6;BYDAY=TH,-1MO", tzmap); System.out.println("\n\n" + test.toString() + "\n-------------------------------------------------"); List<Date> dateList = test.expandRecurrenceOverRange(dtStart, rangeStart.getTime(), rangeEnd.getTime()); for (Date d : dateList) { cal.setTime(d); cal.setTimeZone(tzUTC); System.out.printf("%tc\n", cal); } } catch (ServiceException e) { System.out.println("Caught ServiceException" + e); e.printStackTrace(); } try { ZRecur test = new ZRecur("FREQ=DAILY;BYMONTH=5,6;BYMONTHDAY=1,3,5,7,9,31", tzmap); System.out.println("\n\n" + test.toString() + "\n-------------------------------------------------"); List<Date> dateList = test.expandRecurrenceOverRange(dtStart, rangeStart.getTime(), rangeEnd.getTime()); for (Date d : dateList) { cal.setTime(d); System.out.printf("%tc\n", cal); } } catch (ServiceException e) { System.out.println("Caught ServiceException" + e); e.printStackTrace(); } try { ZRecur test = new ZRecur("FREQ=DAILY;BYMONTH=5,6;BYMONTHDAY=1,3,5,7,9,31;BYDAY=SU,SA", tzmap); System.out.println( "\n\n" + test.toString() + "\n--------------------------------------------------------------"); List<Date> dateList = test.expandRecurrenceOverRange(dtStart, rangeStart.getTime(), rangeEnd.getTime()); for (Date d : dateList) { cal.setTime(d); System.out.printf("%tc\n", cal); } } catch (ServiceException e) { System.out.println("Caught ServiceException" + e); e.printStackTrace(); } try { ZRecur test = new ZRecur("FREQ=DAILY;BYMONTH=5,6;BYMONTHDAY=1,3,5,7,9,31;BYDAY=SU,SA;BYHOUR=21,0", tzmap); System.out.println( "\n\n" + test.toString() + "\n--------------------------------------------------------------"); List<Date> dateList = test.expandRecurrenceOverRange(dtStart, rangeStart.getTime(), rangeEnd.getTime()); for (Date d : dateList) { cal.setTime(d); System.out.printf("%tc\n", cal); } } catch (ServiceException e) { System.out.println("Caught ServiceException" + e); e.printStackTrace(); } try { ZRecur test = new ZRecur( "FREQ=DAILY;BYMONTH=5,6;BYMONTHDAY=1,3,5,7,9,31;BYDAY=SU;BYHOUR=21,0;BYMINUTE=23", tzmap); System.out.println( "\n\n" + test.toString() + "\n--------------------------------------------------------------"); List<Date> dateList = test.expandRecurrenceOverRange(dtStart, rangeStart.getTime(), rangeEnd.getTime()); for (Date d : dateList) { cal.setTime(d); System.out.printf("%tc\n", cal); } } catch (ServiceException e) { System.out.println("Caught ServiceException" + e); e.printStackTrace(); } try { ZRecur test = new ZRecur( "FREQ=DAILY;BYMONTH=5,6;BYMONTHDAY=1,3,5,7,9,31;BYDAY=SU;BYHOUR=1,21,0;BYSECOND=0,59", tzmap); System.out.println( "\n\n" + test.toString() + "\n--------------------------------------------------------------"); List<Date> dateList = test.expandRecurrenceOverRange(dtStart, rangeStart.getTime(), rangeEnd.getTime()); for (Date d : dateList) { cal.setTime(d); System.out.printf("%tc\n", cal); } } catch (ServiceException e) { System.out.println("Caught ServiceException" + e); e.printStackTrace(); } try { // parse error testing ZRecur test = new ZRecur( "FREQ=DAILY;BIYMONTH=5,6;BYMONTHDAY=1,3,5,7,9,31;BYDAY=SU;BYHOUR=1,21,0;BYSECOND=0,59;BYSETPOS=1,-1,3,1000,,-1000", tzmap); System.out.println( "\n\n" + test.toString() + "\n--------------------------------------------------------------"); List<Date> dateList = test.expandRecurrenceOverRange(dtStart, rangeStart.getTime(), rangeEnd.getTime()); for (Date d : dateList) { cal.setTime(d); System.out.printf("%tc\n", cal); } } catch (ServiceException e) { System.out.println("Caught ServiceException" + e); e.printStackTrace(); } try { ZRecur test = new ZRecur("FREQ=HOURLY;BIYMONTH=6;BYMONTHDAY=1,3;BYHOUR=2,14", tzmap); System.out.println( "\n\n" + test.toString() + "\n--------------------------------------------------------------"); List<Date> dateList = test.expandRecurrenceOverRange(dtStart, rangeStart.getTime(), rangeEnd.getTime()); for (Date d : dateList) { cal.setTime(d); System.out.printf("%tc\n", cal); } } catch (ServiceException e) { System.out.println("Caught ServiceException" + e); e.printStackTrace(); } try { ZRecur test = new ZRecur("FREQ=HOURLY;BIYMONTH=6;BYMONTHDAY=1;;BYMINUTE=10;BYSECOND=11,12", tzmap); System.out.println( "\n\n" + test.toString() + "\n--------------------------------------------------------------"); List<Date> dateList = test.expandRecurrenceOverRange(dtStart, rangeStart.getTime(), rangeEnd.getTime()); for (Date d : dateList) { cal.setTime(d); System.out.printf("%tc\n", cal); } } catch (ServiceException e) { System.out.println("Caught ServiceException" + e); e.printStackTrace(); } cal.set(2010, 0, 1, 0, 0, 0); rangeEnd = cal.getTime(); try { ZRecur test = new ZRecur("FREQ=YEARLY", tzmap); System.out.println( "\n\n" + test.toString() + "\n--------------------------------------------------------------"); List<Date> dateList = test.expandRecurrenceOverRange(dtStart, rangeStart.getTime(), rangeEnd.getTime()); for (Date d : dateList) { cal.setTime(d); System.out.printf("%tc\n", cal); } } catch (ServiceException e) { System.out.println("Caught ServiceException" + e); e.printStackTrace(); } try { ZRecur test = new ZRecur("FREQ=YEARLY;BYYEARDAY=-1", tzmap); System.out.println( "\n\n" + test.toString() + "\n--------------------------------------------------------------"); List<Date> dateList = test.expandRecurrenceOverRange(dtStart, rangeStart.getTime(), rangeEnd.getTime()); for (Date d : dateList) { cal.setTime(d); System.out.printf("%tc\n", cal); } } catch (ServiceException e) { System.out.println("Caught ServiceException" + e); e.printStackTrace(); } try { ZRecur test = new ZRecur("FREQ=SECONDLY", tzmap); System.out.println( "\n\n" + test.toString() + "\n--------------------------------------------------------------"); List<Date> dateList = test.expandRecurrenceOverRange(dtStart, rangeStart.getTime(), rangeEnd.getTime()); for (Date d : dateList) { cal.setTime(d); System.out.printf("%tc\n", cal); } } catch (ServiceException e) { System.out.println("Caught ServiceException" + e); e.printStackTrace(); } try { ParsedDateTime myDtStart = ParsedDateTime.parse("16010101T020000", tzmap, tzUTC, tzUTC); ZRecur test = new ZRecur("FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=12;BYDAY=-1SU", tzmap); System.out.println( "\n\n" + test.toString() + "\n--------------------------------------------------------------"); List<Date> dateList = test.expandRecurrenceOverRange(myDtStart, rangeStart.getTime(), rangeEnd.getTime()); for (Date d : dateList) { cal.setTime(d); System.out.printf("%tc\n", cal); } } catch (ParseException e) { System.out.println("Caught ParseException" + e); e.printStackTrace(); } catch (ServiceException e) { System.out.println("Caught ServiceException" + e); e.printStackTrace(); } cal.set(2010, 0, 1, 0, 0, 0); rangeEnd = cal.getTime(); try { ZRecur test = new ZRecur("FREQ=YEARLY;BYMONTH=12;BYDAY=1WE", tzmap); System.out.println( "\n\n" + test.toString() + "\n--------------------------------------------------------------"); List<Date> dateList = test.expandRecurrenceOverRange(dtStart, rangeStart.getTime(), rangeEnd.getTime()); for (Date d : dateList) { cal.setTime(d); System.out.printf("%tc\n", cal); } } catch (ServiceException e) { System.out.println("Caught ServiceException" + e); e.printStackTrace(); } } private List<ZWeekDayNum> mByDayList = new ArrayList<ZWeekDayNum>(); private List<Integer> mByHourList = new ArrayList<Integer>(); private List<Integer> mByMinuteList = new ArrayList<Integer>(); private List<Integer> mByMonthDayList = new ArrayList<Integer>(); private List<Integer> mByMonthList = new ArrayList<Integer>(); private List<Integer> mBySecondList = new ArrayList<Integer>(); private List<Integer> mBySetPosList = new ArrayList<Integer>(); private List<Integer> mByWeekNoList = new ArrayList<Integer>(); private List<Integer> mByYearDayList = new ArrayList<Integer>(); private int mCount = 0; private Frequency mFreq = Frequency.WEEKLY; private int mInterval = 0; private ParsedDateTime mUntil = null; private ZWeekDay mWkSt = null; public ZRecur(String str, TimeZoneMap tzmap) throws ServiceException { parse(str, tzmap); } private ZRecur(ZRecur other) { mByDayList.addAll(other.mByDayList); mByHourList.addAll(other.mByHourList); mByMinuteList.addAll(other.mByMinuteList); mByMonthDayList.addAll(other.mByMonthDayList); mByMonthList.addAll(other.mByMonthList); mBySecondList.addAll(other.mBySecondList); mBySetPosList.addAll(other.mBySetPosList); mByWeekNoList.addAll(other.mByWeekNoList); mByYearDayList.addAll(other.mByYearDayList); mCount = other.mCount; mFreq = other.mFreq; mInterval = other.mInterval; mUntil = other.mUntil == null ? null : (ParsedDateTime) other.mUntil.clone(); mWkSt = other.mWkSt; } @Override public Object clone() { return new ZRecur(this); } public List<Integer> getByHourList() { return this.mByHourList; } public void setByHourList(List<Integer> byHourList) { this.mByHourList = byHourList; } public List<Integer> getByMinuteList() { return this.mByMinuteList; } public void setByMinuteList(List<Integer> byMinuteList) { this.mByMinuteList = byMinuteList; } public List<Integer> getByMonthDayList() { return this.mByMonthDayList; } public void setByMonthDayList(List<Integer> byMonthDayList) { this.mByMonthDayList = byMonthDayList; } public List<Integer> getByMonthList() { return this.mByMonthList; } public void setByMonthList(List<Integer> byMonthList) { this.mByMonthList = byMonthList; } public List<Integer> getBySecondList() { return this.mBySecondList; } public void setBySecondList(List<Integer> bySecondList) { this.mBySecondList = bySecondList; } public List<Integer> getBySetPosList() { return this.mBySetPosList; } public void setBySetPosList(List<Integer> bySetPosList) { this.mBySetPosList = bySetPosList; } public List<Integer> getByWeekNoList() { return this.mByWeekNoList; } public void setByWeekNoList(List<Integer> byWeekNoList) { this.mByWeekNoList = byWeekNoList; } public List<Integer> getByYearDayList() { return this.mByYearDayList; } public void setByYearDayList(List<Integer> byYearDayList) { this.mByYearDayList = byYearDayList; } public int getCount() { return this.mCount; } public void setCount(int count) { this.mCount = count; } public Frequency getFrequency() { return this.mFreq; } public void setFrequency(Frequency freq) { this.mFreq = freq; } public int getInterval() { return this.mInterval; } public void setInterval(int interval) { this.mInterval = interval; } public ParsedDateTime getUntil() { return this.mUntil; } public void setUntil(ParsedDateTime until) { this.mUntil = until; } public ZWeekDay getWkSt() { return this.mWkSt; } public void setWkSt(ZWeekDay wkSt) { this.mWkSt = wkSt; } public void setByDayList(List<ZWeekDayNum> byDayList) { this.mByDayList = byDayList; } public List<ZWeekDayNum> getByDayList() { return this.mByDayList; } public List<Date> expandRecurrenceOverRange(ParsedDateTime dtStart, long rangeStart, long rangeEnd) throws ServiceException { List<Date> toRet = new LinkedList<Date>(); Date rangeStartDate = new Date(rangeStart); // subtract 1000ms (1sec) because the code in the method treats // end time as inclusive while the rangeEnd input argument is // exclusive value Date rangeEndDate = new Date(rangeEnd - 1000); Date dtStartDate = new Date(dtStart.getUtcTime()); Date earliestDate; if (dtStartDate.after(rangeStartDate)) earliestDate = dtStartDate; else earliestDate = rangeStartDate; if (mUntil != null) { Date until = mUntil.getDateForRecurUntil(dtStart.getTimeZone()); if (until.before(rangeEndDate)) rangeEndDate = until; } // Set limit of expansion count. int maxInstancesFromConfig = sExpansionLimits.maxInstances; int maxInstancesExpanded; if (maxInstancesFromConfig <= 0) maxInstancesExpanded = mCount; else if (mCount <= 0) maxInstancesExpanded = maxInstancesFromConfig; else maxInstancesExpanded = Math.min(mCount, maxInstancesFromConfig); int numInstancesExpanded = 1; // initially 1 rather than 0 because DTSTART is always included // Set hard limit of expansion time range. (bug 21989) ParsedDateTime earliestDateTime = ParsedDateTime.fromUTCTime(earliestDate.getTime()); Date hardEndDate = getEstimatedEndTime(earliestDateTime); if (hardEndDate.before(rangeEndDate)) rangeEndDate = hardEndDate; if (rangeEndDate.before(earliestDate)) { ZimbraLog.calendar.debug( "Expanding recurrence over range where range end %s is before earliest date %s", DateUtil.formatDate(rangeEndDate), DateUtil.formatDate(earliestDate)); return toRet; } GregorianCalendar cur = dtStart.getCalendarCopy(); int baseMonthDay = cur.get(Calendar.DAY_OF_MONTH); boolean baseIsLeapDay = ((baseMonthDay == 29) && (cur.get(Calendar.MONTH) == Calendar.FEBRUARY)); // until we hit rangeEnd, or we've SAVED count entries: // // gather each set { // // // // curDate forward one INTERVAL // // } // check Set against BYSETPOS & ranges & count // int interval = mInterval; if (interval <= 0) interval = 1; // DTSTART is always part of the expansion, as long as it falls within // the range. if (!dtStartDate.before(earliestDate) && !dtStartDate.after(rangeEndDate)) toRet.add(dtStartDate); int numConsecutiveIterationsWithoutMatchingInstance = 0; boolean pastHardEndTime = false; long numIterations = 0; // track how many times we looped while (!pastHardEndTime && (maxInstancesExpanded <= 0 || numInstancesExpanded < maxInstancesExpanded)) { numIterations++; boolean curIsAtOrAfterEarliestDate = !cur.getTime().before(earliestDate); boolean curIsAfterEndDate = cur.getTime().after(rangeEndDate); List<Calendar> addList = new LinkedList<Calendar>(); switch (mFreq) { case HOURLY: /* * BYSECOND - for each listed second * BYMINUTE - for each listed minute in hour * BYHOUR - match iff in hour list * BYDAY - for each day listed * BYMONTHDAY - only those monthdays * BYYEARDAY - only those yeardays * BYMONTH - only those months */ if (!checkMonthList(cur)) continue; if (!checkYearDayList(cur)) continue; if (!checkMonthDayList(cur)) continue; if (!checkDayList(cur)) continue; if (!checkHourList(cur)) continue; addList.add((Calendar) (cur.clone())); cur.add(Calendar.HOUR_OF_DAY, interval); addList = expandHourList(addList); addList = expandMinuteList(addList); addList = expandSecondList(addList); break; case DAILY: /* * BYSECOND - for each listed second in day * BYMINUTE - for each listed minute in day * BYHOUR - for each listed hour in day * BYDAY - no ordinal allowed, match iff in day list * BYMONTHDAY - only that day * BYYEARDAY - only that day * BYWEEKNO -- YEARLY ONLY * BYMONTH - only that month * * while (count check & until check & rangeEnd check) { * if (byMonth && !month matches) * curDay = set MONTH to matching month * * if (byYearDay && !yearday matches) * curDay = set DAY to next matching yearday * * if (byMonthday && !monthday matches) * curDay = skip to next matching monthday * * if (byDay && !day in list) * curDay = skip to next mathcing byDay * * if (!byHour or FOR EACH HOUR IN HOURLIST) * if (!byMinute or FOR EACH MINUTE IN MINLIST) * if (!bySecond or FOR EACH SECOND IN LIST) * ----add to list--- * * check against BYSETPOS * * curDay+=1 day * } * */ if (!checkMonthList(cur)) continue; if (!checkYearDayList(cur)) continue; if (!checkMonthDayList(cur)) continue; if (!checkDayList(cur)) continue; addList.add((Calendar) (cur.clone())); cur.add(Calendar.DAY_OF_YEAR, interval); addList = expandHourList(addList); addList = expandMinuteList(addList); addList = expandSecondList(addList); break; case WEEKLY: /* * BYSECOND - for every listed second * BYMINUTE - for every listed minute * BYHOUR - for every listed hour * BYDAY - for every listed day * BYMONTHDAY - MAYBE once a month * BYYEARDAY - MAYBE once a year * BYMONTH - iff month matches * * for each (INTERVAL)WEEK{ * if (byMonth && !month matches) * curDay = set MONTH to DtStart in next matching month * * if (byYearDay && !yearday matches) * curDay = set date to next matching yearday * * if (byMonthDay && !monthday matches) * curDay = skip to next matching monthday * * if (!byDay or FOREACH day in list) * if (!byHour or FOREACH hour in list) * if (!byMinute or FOREACH minute in list) * if (!bySecond or FOREACH second in list) * ----add to list---- * * check against BYSETPOS * * curDay += 1 week * } while (count check & until check & rangeEnd check) * */ if (!checkMonthList(cur)) continue; if (!checkYearDayList(cur)) continue; if (!checkMonthDayList(cur)) continue; addList.add((Calendar) (cur.clone())); cur.add(Calendar.WEEK_OF_YEAR, interval); addList = expandDayListForWeekly(addList); addList = expandHourList(addList); addList = expandMinuteList(addList); addList = expandSecondList(addList); break; case MONTHLY: if (!checkMonthList(cur)) continue; if (!checkYearDayList(cur)) continue; addList.add((Calendar) (cur.clone())); cur.set(Calendar.DAY_OF_MONTH, 1); cur.add(Calendar.MONTH, interval); int daysInMonth = cur.getActualMaximum(Calendar.DAY_OF_MONTH); cur.set(Calendar.DAY_OF_MONTH, Math.min(baseMonthDay, daysInMonth)); addList = expandMonthDayList(addList); addList = expandDayListForMonthlyYearly(addList); addList = expandHourList(addList); addList = expandMinuteList(addList); addList = expandSecondList(addList); break; case YEARLY: /* * BYSECOND * BYMINUTE * BYHOUR * BYDAY * BYMONTHDAY * BYYEARDAY * BYWEEKNO - specified week * BYMONTH - once */ if (baseIsLeapDay) { // previously adding a year to a leap day will have rounded down to the 28th. // If this happened, we need to be sure that if we are back in a leap // year, it is back at 29th cur.set(Calendar.DAY_OF_MONTH, cur.getActualMaximum(Calendar.DAY_OF_MONTH)); } if (ignoreYearForRecurrenceExpansion(cur, baseIsLeapDay)) { cur.add(Calendar.YEAR, interval); break; } addList.add((Calendar) (cur.clone())); cur.add(Calendar.YEAR, interval); addList = expandMonthList(addList); addList = expandYearDayList(addList); addList = expandMonthDayList(addList); addList = expandDayListForMonthlyYearly(addList); addList = expandHourList(addList); addList = expandMinuteList(addList); addList = expandSecondList(addList); break; default: // MINUTELY and SECONDLY are intentionally not supported for performance reasons. return toRet; } addList = handleSetPos(addList); boolean noInstanceFound = true; boolean foundInstancePastEndDate = false; // add all the ones that match! for (Calendar addCal : addList) { Date toAdd = addCal.getTime(); // We already counted DTSTART before the main loop, so don't // count it twice. if (toAdd.compareTo(dtStartDate) == 0) { noInstanceFound = false; continue; } // we still have expanded this instance, even if it isn't in our // current date window if (toAdd.after(dtStartDate)) numInstancesExpanded++; if (!toAdd.after(rangeEndDate)) { if (!toAdd.before(earliestDate)) { toRet.add(toAdd); noInstanceFound = false; } } else { foundInstancePastEndDate = true; break; } if (maxInstancesExpanded > 0 && numInstancesExpanded >= maxInstancesExpanded) break; } // Detect invalid rule. If the rule was invalid the current iteration, which is for the current // frequency interval, would have found no matching instance. The next iteration will also find // no matching instance, and there is no need to keep iterating until we go past the hard end // time or COUNT/UNTIL limit. // // However, we have to make an exception for leap year. An yearly rule looking for February 29th // will find no instance in up to 3 consecutive years before finding Feb 29th in the fourth year. // // So the invalid rule detection must look for at least 4 consecutive failed iterations. if (curIsAtOrAfterEarliestDate) { if (noInstanceFound) numConsecutiveIterationsWithoutMatchingInstance++; else numConsecutiveIterationsWithoutMatchingInstance = 0; if (numConsecutiveIterationsWithoutMatchingInstance >= 4) { ZimbraLog.calendar.warn("Invalid recurrence rule: " + toString()); return toRet; } } pastHardEndTime = foundInstancePastEndDate || (noInstanceFound && curIsAfterEndDate); } return toRet; } @Override public String toString() { StringBuffer toRet = new StringBuffer("FREQ=").append(mFreq); if (mUntil != null) { toRet.append(';').append("UNTIL="); toRet.append(mUntil.getDateTimePartString(false)); } if (mCount > 0) toRet.append(';').append("COUNT=").append(mCount); if (mInterval > 0) toRet.append(';').append("INTERVAL=").append(mInterval); if (mBySecondList.size() > 0) toRet.append(';').append("BYSECOND=").append(listAsStr(mBySecondList)); if (mByMinuteList.size() > 0) toRet.append(';').append("BYMINUTE=").append(listAsStr(mByMinuteList)); if (mByHourList.size() > 0) toRet.append(';').append("BYHOUR=").append(listAsStr(mByHourList)); if (mByDayList.size() > 0) toRet.append(';').append("BYDAY=").append(listAsStr(mByDayList)); if (mByMonthDayList.size() > 0) toRet.append(';').append("BYMONTHDAY=").append(listAsStr(mByMonthDayList)); if (mByYearDayList.size() > 0) toRet.append(';').append("BYYEARDAY=").append(listAsStr(mByYearDayList)); if (mByWeekNoList.size() > 0) toRet.append(';').append("BYWEEKNO=").append(listAsStr(mByWeekNoList)); if (mByMonthList.size() > 0) toRet.append(';').append("BYMONTH=").append(listAsStr(mByMonthList)); if (mBySetPosList.size() > 0) toRet.append(';').append("BYSETPOS=").append(listAsStr(mBySetPosList)); return toRet.toString(); } /** * This version is for HOURLY/DAILY frequencies: it does NOT check the ordinal at all, * it only verifies that the day-of-the-week matches * * @param cal * @return */ private boolean checkDayList(GregorianCalendar cal) { assert (mFreq != Frequency.MONTHLY && mFreq != Frequency.YEARLY && mFreq != Frequency.WEEKLY); if (mByDayList.size() > 0) { for (ZWeekDayNum listCur : mByDayList) { int curDayOfWeek = cal.get(Calendar.DAY_OF_WEEK); if (listCur.mDay.getCalendarDay() == curDayOfWeek) return true; // since the DayOfWeek list is in week-order, if we hit a HIGHER one, // then we know out current one isn't in the list, and therefore // we should go to this one we just found in the list if (listCur.mDay.getCalendarDay() > curDayOfWeek) { cal.set(Calendar.DAY_OF_WEEK, listCur.mDay.getCalendarDay()); return false; } } // we've not found a match AND we've not found a // higher value in our list -- so wrap cal.set(Calendar.DAY_OF_WEEK, mByDayList.get(0).mDay.getCalendarDay()); cal.add(Calendar.WEEK_OF_YEAR, 1); return false; } return true; } /** * @param cal * @return */ private boolean checkHourList(GregorianCalendar cal) { if (mByHourList.size() > 0) { for (Integer cur : mByHourList) { int curHour = cal.get(Calendar.HOUR_OF_DAY); if (curHour == cur.intValue()) return true; // since the month list is in order, if we hit a HIGHER month, // then we know out current month isn't in the list, and therefore // we should go to this next one if (cur > curHour) { cal.set(Calendar.HOUR_OF_DAY, cur); return false; // must re-start checks } } // we've not found a match AND we've not found a // higher value in our list -- so wrap cal.set(Calendar.HOUR, mByHourList.get(0)); cal.add(Calendar.DAY_OF_YEAR, 1); return false; // must re-start checks } return true; } private boolean checkMonthDayList(GregorianCalendar cal) { if (mByMonthDayList.size() > 0) { for (Integer cur : mByMonthDayList) { int curMonthDay = cal.get(Calendar.DAY_OF_MONTH); if (cur == curMonthDay) return true; // since the list is in order, if we hit a HIGHER one, // then we know out current one isn't in the list, and therefore // we should go to this one we just found in the list if (cur > curMonthDay) { cal.set(Calendar.DAY_OF_MONTH, cur); return false; } } // we've not found a match AND we've not found a // higher value in our list -- so wrap cal.set(Calendar.DAY_OF_MONTH, mByMonthDayList.get(0)); cal.add(Calendar.MONTH, 1); return false; } return true; } private boolean checkMonthList(GregorianCalendar cal) { if (mByMonthList.size() > 0) { for (Integer cur : mByMonthList) { int curMonth = cal.get(Calendar.MONTH) + 1; if (cur == curMonth) return true; // since the month list is in order, if we hit a HIGHER month, // then we know out current month isn't in the list, and therefore // we should go to this next one if (cur > curMonth) { cal.set(Calendar.MONTH, cur - 1); return false; // must re-start checks } } // we've not found a match AND we've not found a // higher value in our list -- so wrap cal.set(Calendar.MONTH, mByMonthList.get(0) - 1); cal.add(Calendar.YEAR, 1); return false; // must re-start checks } return true; } private boolean checkYearDayList(GregorianCalendar cal) { if (mByYearDayList.size() > 0) { for (Integer cur : mByYearDayList) { int curYearDay = cal.get(Calendar.DAY_OF_YEAR); if (cur == curYearDay) return true; // since the YearDay list is in order, if we hit a HIGHER one, // then we know out current one isn't in the list, and therefore // we should go to this one we just found in the list if (cur > curYearDay) { cal.set(Calendar.DAY_OF_YEAR, cur); return false; } } // we've not found a match AND we've not found a // higher value in our list -- so wrap cal.set(Calendar.DAY_OF_YEAR, mByYearDayList.get(0)); cal.add(Calendar.YEAR, 1); return false; } return true; } private List<Calendar> expandDayListForMonthlyYearly(List<Calendar> list) { // this func ONLY works for expanding, NOT for contracting assert (mFreq == Frequency.MONTHLY || mFreq == Frequency.YEARLY); if (mByDayList.size() <= 0) return list; List<Calendar> toRet = new ArrayList<Calendar>(); Set<Integer> months = new HashSet<Integer>(); for (Calendar cur : list) { int curYear = cur.get(Calendar.YEAR); int curMonth = cur.get(Calendar.MONTH); if (!months.contains(curMonth)) { months.add(curMonth); for (ZWeekDayNum day : mByDayList) { // find all the cals matching this day-of-week ArrayList<Integer> matching = new ArrayList<Integer>(); cur.set(Calendar.DAY_OF_MONTH, 1); do { if (cur.get(Calendar.DAY_OF_WEEK) == day.mDay.getCalendarDay()) matching.add(cur.get(Calendar.DAY_OF_MONTH)); cur.add(Calendar.DAY_OF_MONTH, 1); } while (cur.get(Calendar.MONTH) == curMonth); cur.set(Calendar.MONTH, curMonth); cur.set(Calendar.YEAR, curYear); if (day.mOrdinal == 0) { for (Integer matchDay : matching) { cur.set(Calendar.DAY_OF_MONTH, matchDay); toRet.add((Calendar) (cur.clone())); } } else { if (day.mOrdinal > 0) { if (day.mOrdinal <= matching.size()) { cur.set(Calendar.DAY_OF_MONTH, matching.get(day.mOrdinal - 1)); toRet.add((Calendar) (cur.clone())); } } else { if ((-1 * day.mOrdinal) <= (matching.size())) { cur.set(Calendar.DAY_OF_MONTH, matching.get(matching.size() + day.mOrdinal)); toRet.add((Calendar) (cur.clone())); } } } } // foreach mByDayList } // month already seen? } // we unfortunately have to sort here because, for example, the "-1FR" could happen before the "-1TH" assert (toRet instanceof ArrayList); Collections.sort(toRet); return toRet; } /** * rfc5545 says : * Recurrence rules may generate recurrence instances with an invalid * date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM * on a day where the local time is moved forward by an hour at 1:00 * AM). Such recurrence instances MUST be ignored and MUST NOT be * counted as part of the recurrence set. * Therefore, ignore this year if the last day isn't 29th for patterns: * RRULE:FREQ=YEARLY;BYMONTHDAY=29;BYMONTH=2 * and "RRULE:FREQ=YEARLY" where DTSTART is a leap day * * @param cur * @param baseIsLeapDay * @return */ private boolean ignoreYearForRecurrenceExpansion(Calendar cur, boolean baseIsLeapDay) { boolean ignoreThisYear = false; if (baseIsLeapDay && (cur.get(Calendar.DAY_OF_MONTH) != 29)) { ignoreThisYear = isSimpleRecurrence(); if (!ignoreThisYear) { if ((mByMonthDayList.size() == 1) && (mByMonthDayList.get(0) == 29)) { ignoreThisYear = (mByMonthList.isEmpty() || ((mByMonthList.size() == 1) && (mByMonthList.get(0) == 2))); } } } return ignoreThisYear; } private boolean isSimpleRecurrence() { if (!mByDayList.isEmpty()) return false; if (!mByHourList.isEmpty()) return false; if (!mByMinuteList.isEmpty()) return false; if (!mByMonthDayList.isEmpty()) return false; if (!mByMonthList.isEmpty()) return false; if (!mBySecondList.isEmpty()) return false; if (!mBySetPosList.isEmpty()) return false; if (!mByWeekNoList.isEmpty()) return false; if (!mByYearDayList.isEmpty()) return false; return true; } /** * Very simple function b/c it can completely ignore the Ordinal value * * @param list * @return */ private List<Calendar> expandDayListForWeekly(List<Calendar> list) { // this func ONLY works for expanding, NOT for contracting assert (mFreq == Frequency.WEEKLY); if (mByDayList.size() <= 0) return list; List<Calendar> toRet = new LinkedList<Calendar>(); for (Calendar cur : list) { for (ZWeekDayNum day : mByDayList) { cur.set(Calendar.DAY_OF_WEEK, day.mDay.getCalendarDay()); toRet.add((Calendar) (cur.clone())); } } return toRet; } private List<Calendar> expandHourList(List<Calendar> list) { // this func ONLY works for expanding, NOT for contracting assert (mFreq == Frequency.DAILY || mFreq == Frequency.WEEKLY || mFreq == Frequency.MONTHLY || mFreq == Frequency.YEARLY); if (mByHourList.size() <= 0) return list; List<Calendar> toRet = new LinkedList<Calendar>(); for (Calendar cur : list) { for (Integer hour : mByHourList) { cur.set(Calendar.HOUR_OF_DAY, hour); toRet.add((Calendar) (cur.clone())); } } return toRet; } private List<Calendar> expandMinuteList(List<Calendar> list) { // this func ONLY works for expanding, NOT for contracting assert (mFreq != Frequency.MINUTELY && mFreq != Frequency.SECONDLY); if (mByMinuteList.size() <= 0) return list; List<Calendar> toRet = new LinkedList<Calendar>(); for (Calendar cur : list) { for (Integer minute : mByMinuteList) { cur.set(Calendar.MINUTE, minute); toRet.add((Calendar) (cur.clone())); } } return toRet; } private List<Calendar> expandMonthDayList(List<Calendar> list) { // this func ONLY works for expanding, NOT for contracting assert (mFreq == Frequency.MONTHLY || mFreq == Frequency.YEARLY); if (mByMonthDayList.size() <= 0) return list; List<Calendar> toRet = new LinkedList<Calendar>(); for (Calendar cur : list) { int curMonth = cur.get(Calendar.MONTH); int lastMonthDay = cur.getActualMaximum(Calendar.DAY_OF_MONTH); boolean seenLastMonthDay = false; for (Integer moday : mByMonthDayList) { if (moday != 0) { if (moday > 0) { if (moday >= lastMonthDay) { if (seenLastMonthDay) continue; seenLastMonthDay = true; moday = lastMonthDay; } cur.set(Calendar.DAY_OF_MONTH, moday); } else { if (moday == -1) { if (seenLastMonthDay) continue; seenLastMonthDay = true; } cur.set(Calendar.DAY_OF_MONTH, 1); cur.roll(Calendar.DAY_OF_MONTH, moday); } assert (cur.get(Calendar.MONTH) == curMonth); toRet.add((Calendar) (cur.clone())); } } } return toRet; } private List<Calendar> expandMonthList(List<Calendar> list) { // this func ONLY works for expanding, NOT for contracting assert (mFreq == Frequency.YEARLY); if (mByMonthList.size() <= 0) return list; List<Calendar> toRet = new LinkedList<Calendar>(); for (Calendar cur : list) { for (Integer month : mByMonthList) { cur.set(Calendar.MONTH, month - 1); toRet.add((Calendar) (cur.clone())); } } return toRet; } private List<Calendar> expandSecondList(List<Calendar> list) { // this func ONLY works for expanding, NOT for contracting assert (mFreq != Frequency.SECONDLY); if (mBySecondList.size() <= 0) return list; List<Calendar> toRet = new LinkedList<Calendar>(); for (Calendar cur : list) { for (Integer second : mBySecondList) { cur.set(Calendar.SECOND, second); toRet.add((Calendar) (cur.clone())); } } return toRet; } private List<Calendar> expandYearDayList(List<Calendar> list) { // this func ONLY works for expanding, NOT for contracting assert (mFreq == Frequency.YEARLY); if (mByYearDayList.size() <= 0) return list; List<Calendar> toRet = new LinkedList<Calendar>(); Set<Integer> years = new HashSet<Integer>(); for (Calendar cur : list) { int curYear = cur.get(Calendar.YEAR); if (!years.contains(curYear)) { years.add(curYear); for (Integer yearDay : mByYearDayList) { if (yearDay > 0) cur.set(Calendar.DAY_OF_YEAR, yearDay); else { cur.set(Calendar.DAY_OF_YEAR, 1); cur.roll(Calendar.DAY_OF_YEAR, yearDay); } toRet.add((Calendar) (cur.clone())); } } // year already seen? } return toRet; } private List<Calendar> handleSetPos(List<Calendar> list) { if (mBySetPosList.size() <= 0) return list; Calendar[] array = new Calendar[list.size()]; array = list.toArray(array); LinkedList<Calendar> toRet = new LinkedList<Calendar>(); ArrayList<Integer> idxsToInclude = new ArrayList<Integer>(); for (Integer cur : mBySetPosList) { int idx = cur; if (idx >= -366 && idx <= 366 && idx != 0) { if (idx > 0) idx--; // 1-indexed! else idx = array.length + idx; if (idx >= 0 && idx < array.length) if (!idxsToInclude.contains(idx)) idxsToInclude.add(idx); } } Collections.sort(idxsToInclude); for (Integer idx : idxsToInclude) { toRet.add(array[idx]); } return toRet; } private void parse(String str, TimeZoneMap tzmap) throws ServiceException { try { int numByParts = 0; for (String tok : str.split("\\s*;\\s*")) { String[] s = tok.split("\\s*=\\s*"); if (s.length != 2) { if (ZimbraLog.calendar.isDebugEnabled()) ZimbraLog.calendar.debug( new Formatter().format("Parse error for recur: \"%s\" at token \"%s\"", str, tok)); continue; } String rhs = s[1]; // MS Exchange can add invalid spaces in RRULE part values. Get rid of them. (see bug 25169) rhs = rhs.replaceAll("\\s+", ""); try { switch (Tokens.valueOf(s[0])) { case FREQ: mFreq = Frequency.valueOf(rhs); break; case UNTIL: ParsedDateTime until = ParsedDateTime.parse(rhs, tzmap); if (until != null) { // RFC2445 4.3.10 Recurrence Rule: // "If specified as a date-time value, then it MUST // be specified in an UTC time format." if (until.hasTime()) until.toUTC(); mUntil = until; } break; case COUNT: mCount = Integer.parseInt(rhs); break; case INTERVAL: mInterval = Integer.parseInt(rhs); break; case BYSECOND: ++numByParts; parseIntList(rhs, mBySecondList, 0, 59, false); break; case BYMINUTE: ++numByParts; parseIntList(rhs, mByMinuteList, 0, 59, false); break; case BYHOUR: ++numByParts; parseIntList(rhs, mByHourList, 0, 23, false); break; case BYDAY: ++numByParts; parseByDayList(rhs, mByDayList); break; case BYMONTHDAY: ++numByParts; parseIntList(rhs, mByMonthDayList, -31, 31, true); break; case BYYEARDAY: ++numByParts; parseIntList(rhs, mByYearDayList, -366, 366, true); break; case BYWEEKNO: ++numByParts; parseIntList(rhs, mByWeekNoList, -53, 53, true); break; case BYMONTH: ++numByParts; parseIntList(rhs, mByMonthList, 1, 12, false); break; case BYSETPOS: ++numByParts; parseIntList(rhs, mBySetPosList, Integer.MIN_VALUE, Integer.MAX_VALUE, true); break; case WKST: mWkSt = ZWeekDay.valueOf(rhs); break; } } catch (IllegalArgumentException e) { ZimbraLog.calendar.warn("Skipping RECUR token: \"%s\" in Recur \"%s\" due to parse error", s[0], str, e); } } // Convert monthly BYDAY+BYSETPOS combo into simpler BYDAY format. (bug 35568, 59771) // e.g. "BYDAY=TU;BYSETPOS=3" becomes "BYDAY=3TU". // This is done only in a very specific case, when all of the following conditions are met: // 1) recurrence is monthly // 2) BYDAY has exactly one element // 3) the BYDAY element has no ordinal // 4) BYSETPOS has exactly one element // 5) there is no other BYxxx part if (numByParts == 2 && Frequency.MONTHLY.equals(mFreq) && mByDayList.size() == 1 && mByDayList.get(0).mOrdinal == 0 && mBySetPosList.size() == 1) { mByDayList.get(0).mOrdinal = mBySetPosList.get(0); mBySetPosList.clear(); } } catch (ParseException e) { throw ServiceException.FAILURE("Parse error for recur \"" + str + "\"", e); } } private static int parseSignedInt(String str) { if (str == null) throw new NumberFormatException("null is not a number"); int len = str.length(); if (len == 0) throw new NumberFormatException("empty string is not a number"); int num = 0; if (str.charAt(0) == '+') { if (len == 1) throw new NumberFormatException("+ is not a number"); num = Integer.parseInt(str.substring(1)); } else { num = Integer.parseInt(str); } return num; } private static int parseUnsignedInt(String str) { if (str == null) throw new NumberFormatException("null is not a number"); int len = str.length(); if (len == 0) throw new NumberFormatException("empty string is not a number"); char sign = str.charAt(0); if (sign == '+' || sign == '-') throw new NumberFormatException("sign not allowed: " + str); return Integer.parseInt(str); } private void parseByDayList(String str, List<ZWeekDayNum> list) { for (String s : str.split("\\s*,\\s*")) { ZWeekDayNum wdn = new ZWeekDayNum(); String dayStr = s; if (s.length() > 2) { String numStr = s.substring(0, s.length() - 2); dayStr = dayStr.substring(s.length() - 2); wdn.mOrdinal = parseSignedInt(numStr); } wdn.mDay = ZWeekDay.valueOf(dayStr); list.add(wdn); } // sort by DAY-OF-WEEK (necessary for the byDay checks to work) Collections.sort(list, new ZWeekDayNum.DayOnlyComparator()); } private void parseIntList(String str, List<Integer> list, int min, int max, boolean signed) { for (String s : str.split("\\s*,\\s*")) try { int readInt; if (signed) readInt = parseSignedInt(s); else readInt = parseUnsignedInt(s); if (readInt >= min && readInt <= max) { list.add(readInt); } } catch (Exception e) { ZimbraLog.calendar.debug(new Formatter().format( "Skipping unparsable Recur int list entry: \"%s\" in parameter list: \"%s\"", s, str)); } Collections.sort(list); } }