org.dmfs.rfc5545.recur.RecurrenceRule Maven / Gradle / Ivy
/*
* Copyright (C) 2013 Marten Gajda
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.dmfs.rfc5545.recur;
import org.dmfs.rfc5545.DateTime;
import org.dmfs.rfc5545.Weekday;
import org.dmfs.rfc5545.calendarmetrics.CalendarMetrics;
import org.dmfs.rfc5545.calendarmetrics.CalendarMetrics.CalendarMetricsFactory;
import org.dmfs.rfc5545.calendarmetrics.GregorianCalendarMetrics;
import java.util.*;
import java.util.Map.Entry;
/**
* Builder and parser for recurrence rule strings that comply with RFC 2445 or RFC 5545. The goal of this implementation is to satisfy the following qualities:
* - correctness: The instances returned by the iterator shall be correct for all common cases, i.e. they follow all rules defined in RFC 5545/RFC 2445.
* - completeness: The iterator shall support all valid combinations defined by RFC 5545 and RFC 2445 and return reasonable results for edge cases that are
* not explicitly mentioned.
- performance: The iterator shall be as efficient (in speed and memory utilization) as possible.
TODO: Add
* validator and a validator log.
TODO: Add proper implementation of the {@link #equals(Object)} method.
TODO: Add support for jCal rules.
*
*
* @author Marten Gajda
*/
public final class RecurrenceRule
{
/**
* Enumeration of supported rule versions.
*/
public enum RfcMode
{
/**
* Parses recurrence rules according to RFC 2445. Every error will cause an exception to
* be thrown.
*/
RFC2445_STRICT(false),
/**
* Parses recurrence rules according to RFC 2445 in a more tolerant way. The parser will
* just skip invalid parts in the rule and won't complain as long as the result is a valid rule. This mode also accepts rules that comply with RFC 5545.
Note: Using this mode rules are evaluated
* differently than with {@link #RFC5545_LAX}. {@link #RFC5545_LAX} will just drop all invalid parts and evaluate the rule according to RFC 5545. This
* mode will evaluate all rules.
Also this mode will output rules that comply with RFC 2445.
*/
RFC2445_LAX(true),
/**
* Parses recurrence rules according to RFC 5545. Every error will cause an exception to
* be thrown.
*/
RFC5545_STRICT(false),
/**
* Parses recurrence rules according to RFC 5545 in a more tolerant way. The parser will
* just skip invalid parts in the rule and won't complain as long as the result is a valid rule. This mode also accepts rules that comply with RFC 2445 but not with RFC 5545.
Note: Using this mode rules
* are evaluated differently than with {@link #RFC2445_LAX}. This mode will just drop all invalid parts and evaluate the rule according to RFC 5545.
* {@link #RFC2445_LAX} will evaluate all rules.
Also this mode will output rules that comply with RFC 5545.
*/
RFC5545_LAX(true);
final boolean mIsLax;
RfcMode(boolean isLax)
{
mIsLax = isLax;
}
}
/**
* Values of the new SKIP parameter as added in tools.ietf.org/html/draft-daboo-icalendar-rscale-03
*/
public enum Skip
{
/**
* OMIT is the default value. It means that non-existing dates are just ignored.
*/
OMIT,
/**
* BACKWARD means that non-existing instanced get rolled back to the previous day (for leap days) or month (for leap months).
*/
BACKWARD,
/**
* FORWARD means that non-existing instanced get rolled forward to the next day (for leap days) or month (for leap months).
*/
FORWARD;
}
/**
* Enumeration of valid recurrence rule parts. Each of these parts may occur once in a rule. {@link #FREQ} is the only mandatory part. Each part has a
* {@link ValueConverter} that knows how to parse and serialize the values the part can have. Also each part has a factory method to return a {@link
* RuleIterator} for this part. {@link #FREQ}, {@link #INTERVAL}, {@link #WKST} and {@link #RSCALE} don't support iteration nor expansion and will throw an
* {@link UnsupportedOperationException} when calling {@link Part#getExpander(RecurrenceRule, RuleIterator, CalendarMetrics, long, TimeZone)} or Part{@link
* #getFilter(RecurrenceRule, CalendarMetrics)}.
*/
public enum Part
{
/**
* Base frequency of the recurring instances. This value is mandatory in every recurrence rule. The value must be a {@link Freq}.
*/
FREQ(new FreqConverter())
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
return new FreqIterator(rule, calendarMetrics, start);
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
throw new UnsupportedOperationException("FREQ doesn't have a filter.");
}
@Override
boolean expands(RecurrenceRule rule)
{
// the frequency generator always expands
return true;
}
},
/**
* The base interval of the recurring instances. If not specified the interval is 1
. The value must be a positive integer.
*/
INTERVAL(new IntegerConverter(1, Integer.MAX_VALUE))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
throw new UnsupportedOperationException("INTERVAL doesn't have an iterator.");
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
throw new UnsupportedOperationException("INTERVAL doesn't have a filter.");
}
@Override
boolean expands(RecurrenceRule rule)
{
throw new UnsupportedOperationException("INTERVAL doesn't support expansion nor filtering");
}
},
/**
* RSCALE defines the calendar scale to apply. It has been introduced in http://tools.ietf.org/html/draft-daboo-icalendar-rscale-03
*/
RSCALE(new RScaleConverter())
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
throws UnsupportedOperationException
{
throw new UnsupportedOperationException("RSCALE doesn't have an expander.");
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
throw new UnsupportedOperationException("RSCALE doesn't have a filter.");
}
@Override
boolean expands(RecurrenceRule rule)
{
throw new UnsupportedOperationException("RSCALE doesn't support expansion nor filtering");
}
},
/**
* The start day of a week. The value must be a {@link Weekday}. This is relevant if any of {@link Part#BYDAY} or {@link Part#BYWEEKNO} are present.
*/
WKST(new WeekdayConverter())
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
throw new UnsupportedOperationException("WKST doesn't have an iterator.");
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
throw new UnsupportedOperationException("WKST doesn't have a filter.");
}
@Override
boolean expands(RecurrenceRule rule)
{
throw new UnsupportedOperationException("WKST doesn't support expansion nor filtering.");
}
},
/**
* A list of months that specify in which months the instances recur. The value is a list of non-zero integers. The actual values depend on the calendar
* scale and need to be validated after parsing.
*
* TODO: validate month numbers.
*/
BYMONTH(new ListValueConverter(new MonthConverter()))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
return new ByMonthExpander(rule, previous, calendarMetrics, start);
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
if (rule.getFreq() == Freq.WEEKLY && (rule.hasPart(Part.BYDAY) || rule.hasPart(
Part.BYMONTHDAY) || rule.hasPart(Part.BYYEARDAY)))
{
// the rule has a weekly scope and "By*day" filters, so we have to retain weeks that overlap this month
return new ByMonthFilter(rule, calendarMetrics);
}
return new TrivialByMonthFilter(rule);
}
@Override
boolean expands(RecurrenceRule rule)
{
return rule.getFreq() == Freq.YEARLY;
}
},
/**
* The SKIP filter for months. This must not appear in an RRULE, any attempt to parse a value will fail. This is just an implementation helper.
*/
_BYMONTHSKIP(ERROR_CONVERTER)
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
throws UnsupportedOperationException
{
return new ByMonthSkipFilter(rule, previous, calendarMetrics, start);
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
throw new UnsupportedOperationException("_BYMONTHSKIP doesn't support filtering");
}
@Override
boolean expands(RecurrenceRule rule)
{
return true;
}
},
/**
* A list of week numbers that specify in which weeks the instances recur.
*
* TODO: validate week numbers
*/
BYWEEKNO(new ListValueConverter(new IntegerConverter(-53, 53).noZero()))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarTools, long start, TimeZone startTimeZone)
{
ByExpander.Scope scope = rule.hasPart(Part.BYMONTH) ? ByExpander.Scope.MONTHLY : ByExpander.Scope.YEARLY;
// allow overlapping weeks in MONTHLY scope and if any BY*DAY rule is present
boolean allowOverlappingWeeks = scope == ByExpander.Scope.MONTHLY && (rule.hasPart(Part.BYDAY) || rule.hasPart(
Part.BYMONTHDAY) || rule.hasPart(Part.BYYEARDAY));
switch (scope)
{
case MONTHLY:
if (allowOverlappingWeeks)
{
return new ByWeekNoMonthlyOverlapExpander(rule, previous, calendarTools, start);
}
return new ByWeekNoMonthlyExpander(rule, previous, calendarTools, start);
case YEARLY:
return new ByWeekNoYearlyExpander(rule, previous, calendarTools, start);
default:
throw new Error("Illegal scope");
}
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
// no filter defined
return null;
}
@Override
boolean expands(RecurrenceRule rule)
{
// byweekno always expands
return true;
}
},
/**
* A list of year days that specify on which year days the instances recur. The actual limits depend on the calendar scale and needs to be validated
* after parsing. Negative values are supported only if {@link #RSCALE} is present.
*
* TODO: validate year days
*/
BYYEARDAY(new ListValueConverter<>(new IntegerConverter(-366, 366).noZero()))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
// RFC 5545 only allows BYYEARDAY expansion for YEARLY rules
// We'll expand it the same way for WEEKLY and MONTHLY though and filter afterwards for other frequencies if allowed by the mode
return new ByYearDayYearlyExpander(rule, previous, calendarMetrics, start);
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
return new ByYearDayFilter(rule, calendarMetrics);
}
@Override
boolean expands(RecurrenceRule rule)
{
// expand in a yearly, monthly or weekly scope
Freq freq = rule.getFreq();
return freq == Freq.YEARLY || freq == Freq.MONTHLY || freq == Freq.WEEKLY;
}
},
/**
* A list of month days on which the event recurs. Valid values are non-zero integers. The actual limits depend on the calendar scale and needs to be
* validated after parsing.
*
* TODO: validate month days
*/
BYMONTHDAY(new ListValueConverter(new IntegerConverter(-31, 31).noZero()))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
ByExpander.Scope scope = rule.hasPart(Part.BYWEEKNO) || rule.getFreq() == Freq.WEEKLY ? (rule.hasPart(
Part.BYMONTH) || rule.getFreq() == Freq.MONTHLY ? ByExpander.Scope.WEEKLY_AND_MONTHLY
: ByExpander.Scope.WEEKLY)
: ByExpander.Scope.MONTHLY;
switch (scope)
{
case MONTHLY:
return new ByMonthDayMonthlyExpander(rule, previous, calendarMetrics, start);
case WEEKLY:
return new ByMonthDayWeeklyExpander(rule, previous, calendarMetrics, start);
case WEEKLY_AND_MONTHLY:
return new ByMonthDayWeeklyAndMonthlyExpander(rule, previous, calendarMetrics, start);
default:
throw new Error("Illegal Scope");
}
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
return new ByMonthDayFilter(rule, calendarMetrics);
}
@Override
boolean expands(RecurrenceRule rule)
{
// expand in a yearly, monthly or weekly scope if byyearday is not present
Freq freq = rule.getFreq();
return (freq == Freq.YEARLY || freq == Freq.MONTHLY || freq == Freq.WEEKLY /* for RFC 2445 */) && !rule.hasPart(Part.BYYEARDAY);
}
},
/**
* The SKIP filter for monthdays. This must not appear in an RRULE, any attempt to parse a value will fail. This is just an implementation helper.
*/
_BYMONTHDAYSKIP(ERROR_CONVERTER)
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
throws UnsupportedOperationException
{
return new ByMonthDaySkipFilter(rule, previous, calendarMetrics, start);
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
throw new UnsupportedOperationException("_BYMONTHDAYSKIP doesn't support filtering");
}
@Override
boolean expands(RecurrenceRule rule)
{
return true;
}
},
/**
* A list of {@link WeekdayNum}s on which the event recurs.
*/
BYDAY(new ListValueConverter(new WeekdayNumConverter()))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
boolean hasByMonth = rule.hasPart(Part.BYMONTH);
Freq freq = rule.getFreq();
ByExpander.Scope scope = rule.hasPart(
Part.BYWEEKNO) || freq == Freq.WEEKLY ? (hasByMonth || freq == Freq.MONTHLY
? ByExpander.Scope.WEEKLY_AND_MONTHLY
: ByExpander.Scope.WEEKLY)
: (hasByMonth || freq == Freq.MONTHLY ? ByExpander.Scope.MONTHLY : ByExpander.Scope.YEARLY);
switch (scope)
{
case WEEKLY:
return new ByDayWeeklyExpander(rule, previous, calendarMetrics, start);
case WEEKLY_AND_MONTHLY:
return new ByDayWeeklyAndMonthlyExpander(rule, previous, calendarMetrics, start);
case MONTHLY:
return new ByDayMonthlyExpander(rule, previous, calendarMetrics, start);
case YEARLY:
return new ByDayYearlyExpander(rule, previous, calendarMetrics, start);
default:
throw new Error("Illegal scope");
}
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
Freq freq = rule.getFreq();
// TODO: this method looks ugly and can use some improvement
// TODO: consider doing this separation at parsing time
Set nonPrefixed = EnumSet.noneOf(Weekday.class);
Map> prefixed = new EnumMap<>(Weekday.class);
for (WeekdayNum wn : rule.getByDayPart())
{
if (wn.pos == 0)
{
nonPrefixed.add(wn.weekday);
}
else
{
Set prefixes = prefixed.get(wn.weekday);
if (prefixes == null) // TODO use java 8 API, check Android support first
{
prefixes = new HashSet<>();
prefixed.put(wn.weekday, prefixes);
}
prefixes.add(wn.pos);
}
}
if (prefixed.isEmpty()
|| (freq != Freq.YEARLY && freq != Freq.MONTHLY)
|| (freq == Freq.YEARLY && rule.hasPart(BYWEEKNO)))
{
return new ByDayFilter(calendarMetrics, nonPrefixed);
}
ByFilter baseFilter = new ByDayPrefixedFilter(
calendarMetrics,
prefixed,
freq == Freq.YEARLY && rule.getByPart(BYMONTH) == null ?
ByDayPrefixedFilter.Scope.YEAR :
ByDayPrefixedFilter.Scope.MONTH);
if (nonPrefixed.isEmpty())
{
return baseFilter;
}
ByFilter nonPrefixFilter = new ByDayFilter(calendarMetrics, nonPrefixed);
return instance -> nonPrefixFilter.filter(instance) && baseFilter.filter(instance);
}
@Override
boolean expands(RecurrenceRule rule)
{
// expands in a yearly or monthly scope if neither byyearday nor bymonthday are present and in a weekly scope.
Freq freq = rule.getFreq();
return ((freq == Freq.YEARLY || freq == Freq.MONTHLY) && !rule.hasPart(
Part.BYYEARDAY) && !rule.hasPart(Part.BYMONTHDAY))
|| freq == Freq.WEEKLY;
}
},
/**
* A special BYMONTH filter for expander rewriting
*/
_BYMONTH_FILTER(new ListValueConverter<>(new MonthConverter()))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
throw new Error("Unexpected expander request");
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
return new TrivialByMonthFilter(rule);
}
@Override
boolean expands(RecurrenceRule rule)
{
return false;
}
},
/**
* A special BYWEEKNO filter for expander rewriting
*/
_BYWEEKNO_FILTER(new ListValueConverter<>(new IntegerConverter(-53, 53).noZero()))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarTools, long start, TimeZone startTimeZone)
{
throw new Error("Unexpected Expansion request");
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
return new ByWeekNoFilter(rule, calendarMetrics);
}
@Override
boolean expands(RecurrenceRule rule)
{
return false;
}
},
/**
* A special BYYEARDAY filter for expander rewriting
*/
_BYYEARDAY_FILTER(new ListValueConverter<>(new IntegerConverter(-366, 366).noZero()))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
throw new Error("Unexpected expander request");
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
return new ByYearDayFilter(rule, calendarMetrics);
}
@Override
boolean expands(RecurrenceRule rule)
{
return false;
}
},
/**
* A special BYMONTHDAY filter for expander rewriting
*/
_BYMONTHDAY_FILTER(new ListValueConverter<>(new IntegerConverter(-31, 31).noZero()))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
throw new Error("This filter does not expand.");
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
return new ByMonthDayFilter(rule, calendarMetrics);
}
@Override
boolean expands(RecurrenceRule rule)
{
return false;
}
},
/**
* A special BYDAY filter for expander rewriting
*/
_BYDAY_FILTER(new ListValueConverter<>(new WeekdayNumConverter()))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
throw new Error("Unexpected expansion request");
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
return BYDAY.getFilter(rule, calendarMetrics);
}
@Override
boolean expands(RecurrenceRule rule)
{
return false;
}
},
/**
* The hours on which the event recurs. The value must be a list of integers in the range 0 to 23.
*/
BYHOUR(new ListValueConverter(new IntegerConverter(0, 23)))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarTools, long start, TimeZone startTimeZone)
{
return new ByHourExpander(rule, previous, calendarTools, start);
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
return new ByHourFilter(rule);
}
@Override
boolean expands(RecurrenceRule rule)
{
// expands whenever the scope is larger than an hour
Freq freq = rule.getFreq();
return freq != Freq.SECONDLY && freq != Freq.MINUTELY && freq != Freq.HOURLY;
}
},
/**
* The minutes on which the event recurs. The value must be a list of integers in the range 0 to 59.
*/
BYMINUTE(new ListValueConverter(new IntegerConverter(0, 59)))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarTools, long start, TimeZone startTimeZone)
{
return new ByMinuteExpander(rule, previous, calendarTools, start);
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
return new ByMinuteFilter(rule);
}
@Override
boolean expands(RecurrenceRule rule)
{
// expands whenever the scope is larger than a minute
Freq freq = rule.getFreq();
return freq != Freq.SECONDLY && freq != Freq.MINUTELY;
}
},
/**
* The seconds on which the event recurs. The value must be a list of integers in the range 0 to 60.
*/
BYSECOND(new ListValueConverter(new IntegerConverter(0, 60)))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarTools, long start, TimeZone startTimeZone)
{
return new BySecondExpander(rule, previous, calendarTools, start);
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
return new BySecondFilter(rule);
}
@Override
boolean expands(RecurrenceRule rule)
{
// expands whenever the scope is larger than a second
return rule.getFreq() != Freq.SECONDLY;
}
},
/**
* SKIP defines how to handle instances that would fall on a leap day or leap month in a non-leap year. Legal values are defined in {@link Skip}. It has
* been introduced by http://tools.ietf.org/html/draft-daboo-icalendar-rscale-03
*
* Skipping is implemented by an expander because it might modify instances which is not supported by filters.
*/
SKIP(new SkipValueConverter())
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
/*
* The only case when we need the buffer is when rolling a month in a yearly rule forward, because the instance may end up in the next interval
* (i.e. the next year). A leap month can not be the first month in a year, hence it's impossible that we roll a instance backwards to the last
* year.
*
* Leap days may be rolled forward or backwards, but only to the first/last day of the next/previous month, which will be handled by the
* SanityFilter.
*/
if (rule.getFreq() == Freq.YEARLY && rule.getSkip() == Skip.FORWARD)
{
return new SkipBuffer(rule, previous, calendarMetrics);
}
return null;
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
throw new UnsupportedOperationException("SKIP doesn't support filtering");
}
@Override
boolean expands(RecurrenceRule rule)
{
return true;
}
},
/**
* Filters all invalid dates. This must not appear in an RRULE, any attempt to parse a value will fail. This is just an implementation helper.
*/
_SANITY_FILTER(ERROR_CONVERTER)
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
throws UnsupportedOperationException
{
// note: despite it's name the SanityFilter is implemented as an expander
return new SanityFilter(previous, calendarMetrics, start);
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
throw new UnsupportedOperationException("_SANITY doesn't support filtering");
}
@Override
boolean expands(RecurrenceRule rule)
{
return true;
}
},
/**
* A list of set positions to consider when iterating the instances. The value is a list of integers. For now we accept any reasonable value.
*
* TODO: validate the values. They should be within the limits of byyearday.
*/
BYSETPOS(new ListValueConverter(new IntegerConverter(-500, 500).noZero()))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarTools, long start, TimeZone startTimeZone)
{
return new BySetPosFilter(rule, previous, start);
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
throw new UnsupportedOperationException("BYSETPOS doesn't support filtering");
}
@Override
boolean expands(RecurrenceRule rule)
{
return true;
}
},
/**
* This part specifies the latest date of the last instance. The value is a {@link DateTime} in UTC or the local time zone. This part is mutually
* exclusive with {@link #COUNT}. If neither {@link #UNTIL} nor {@link #COUNT} are specified the instances recur forever.
*/
UNTIL(new DateTimeConverter())
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
{
return rule.mode.mIsLax && rule.getUntil() != null && rule.getUntil().isAllDay()
? new UntilDateLimiter(rule, previous)
: new UntilLimiter(rule, previous, startTimeZone);
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
throw new UnsupportedOperationException("UNTIL doesn't support filtering");
}
@Override
boolean expands(RecurrenceRule rule)
{
return true;
}
},
/**
* This part specifies total number of instances. The value is a positive integer. This part is mutually exclusive with {@link #UNTIL}. If neither
* {@link #COUNT} nor {@link #UNTIL} are specified the instances recur forever.
*/
COUNT(new IntegerConverter(1, Integer.MAX_VALUE))
{
@Override
RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarTools, long start, TimeZone startTimeZone)
{
return new CountLimiter(rule, previous);
}
@Override
ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException
{
throw new UnsupportedOperationException("COUNT doesn't support filtering");
}
@Override
boolean expands(RecurrenceRule rule)
{
return true;
}
};
/**
* A {@link ValueConverter} that can parse and serialize the value for a specific part. The generic type depends on the actual part.
*/
final ValueConverter> converter;
/**
* Private constructor.
*
* @param converter
* The {@link ValueConverter} that knows how to parse and serialize this part.
*/
private Part(ValueConverter> converter)
{
this.converter = converter;
}
/**
* Return a {@link RuleIterator} that is suitable to build a recurrence rule filter chain in order to iterate all instances. Note:
* {@link #FREQ}, {@link #INTERVAL}, {@link #RSCALE} and {@link #WKST} don't support this method and throw an {@link UnsupportedOperationException}.
*
*
* @param rule
* The rule to iterate.
* @param previous
* The previous element in the filter chain.
* @param calendarMetrics
* The {@link CalendarMetrics} to use.
* @param start
* The first instance of the event.
* @param startTimeZone
* The {@link TimeZone} this event is in.
*
* @return The {@link RuleIterator} for this part.
*
* @throws UnsupportedOperationException
* If this part does not have a {@link RuleIterator}.
*/
abstract RuleIterator getExpander(RecurrenceRule rule, RuleIterator previous, CalendarMetrics calendarMetrics, long start, TimeZone startTimeZone)
throws UnsupportedOperationException;
/**
* Return a {@link ByFilter}. Note: {@link #FREQ}, {@link #INTERVAL}, {@link #RSCALE} and {@link #WKST} don't support this method
* and throw an {@link UnsupportedOperationException}.
*
* @param rule
* The rule to iterate.
* @param calendarMetrics
* The {@link CalendarMetrics} to use.
*
* @return The {@link RuleIterator} for this part or null
if the give rule doesn't need this part.
*
* @throws UnsupportedOperationException
* If this part does not have a {@link ByFilter}.
*/
abstract ByFilter getFilter(RecurrenceRule rule, CalendarMetrics calendarMetrics) throws UnsupportedOperationException;
/**
* Returns whether this part expands instances or not.
*
* @param rule
* The rule this part belongs to.
*
* @return true
if this rule expands instances, false
if it filters instances for the given rule.
*/
abstract boolean expands(RecurrenceRule rule);
}
private final static Set REWRITE_PARTS = EnumSet.of(Part.BYMONTH, Part.BYWEEKNO, Part.BYYEARDAY, Part.BYMONTHDAY, Part.BYDAY);
private final static Map, Set> YEAR_REWRITE_MAP = new HashMap<>(32);
static
{
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYYEARDAY, Part.BYMONTHDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTHDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYYEARDAY, Part.BYMONTHDAY, Part.BYDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTHDAY_FILTER, Part._BYDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYWEEKNO, Part.BYYEARDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYWEEKNO_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYWEEKNO, Part.BYYEARDAY, Part.BYDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYWEEKNO_FILTER, Part._BYDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYWEEKNO, Part.BYYEARDAY, Part.BYMONTHDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYWEEKNO_FILTER, Part._BYMONTHDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYWEEKNO, Part.BYYEARDAY, Part.BYMONTHDAY, Part.BYDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYWEEKNO_FILTER, Part._BYMONTHDAY_FILTER, Part._BYDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYMONTH, Part.BYYEARDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTH_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYMONTH, Part.BYYEARDAY, Part.BYDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTH_FILTER, Part._BYDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYMONTH, Part.BYYEARDAY, Part.BYMONTHDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTH_FILTER, Part._BYMONTHDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYMONTH, Part.BYYEARDAY, Part.BYMONTHDAY, Part.BYDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTH_FILTER, Part._BYMONTHDAY_FILTER, Part._BYDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYMONTH, Part.BYWEEKNO, Part.BYYEARDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTH_FILTER, Part._BYWEEKNO_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYMONTH, Part.BYWEEKNO, Part.BYYEARDAY, Part.BYDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTH_FILTER, Part._BYWEEKNO_FILTER, Part._BYDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYMONTH, Part.BYWEEKNO, Part.BYYEARDAY, Part.BYMONTHDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTH_FILTER, Part._BYWEEKNO_FILTER, Part._BYMONTHDAY_FILTER));
YEAR_REWRITE_MAP.put(EnumSet.of(Part.BYMONTH, Part.BYWEEKNO, Part.BYYEARDAY, Part.BYMONTHDAY, Part.BYDAY),
EnumSet.of(Part.BYYEARDAY, Part._BYMONTH_FILTER, Part._BYWEEKNO_FILTER, Part._BYMONTHDAY_FILTER, Part._BYDAY_FILTER));
}
/**
* This class represents the position of a {@link Weekday} in a specific range. It parses values like -4SU
which means the fourth last Sunday
* in the interval or 2MO
which means the second Monday in the interval. In addition this class accepts simple weekdays like SU
* which means every Sunday in the interval. These values are defined as:
*
*
* weekdaynum = [[plus / minus] ordwk] weekday
*
* plus = "+"
*
* minus = "-"
*
* ordwk = 1*2DIGIT ;1 to 53
*
* weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA"
* ;Corresponding to SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY,
* ;FRIDAY, and SATURDAY days of the week.
*
*/
public static class WeekdayNum
{
/**
* The position of this weekday in the interval. This value is 0
if this instance means every occurrence of {@link #weekday} in the
* interval.
*/
public final int pos;
/**
* The {@link Weekday}.
*/
public final Weekday weekday;
/**
* Create a new WeekdayNum instance.
*
* TODO: update range check
*
* @param pos
* The position of the weekday in the Interval or 0
for every occurrence of the weekday.
* @param weekday
* The {@link Weekday}.
*/
public WeekdayNum(int pos, Weekday weekday)
{
if (pos < -53 || pos > 53)
{
throw new IllegalArgumentException("position " + pos + " of week day out of range");
}
this.pos = pos;
this.weekday = weekday;
}
/**
* Parse a weekdaynum String as defined in RFC 5545 (this definition equals the
* definition in RFC 2445).
*
* @param value
* The weekdaynum String to parse.
* @param tolerant
* Set to true
to be tolerant and accept values outside of the allowed range.
*
* @return A new {@link WeekdayNum} instance.
*
* @throws InvalidRecurrenceRuleException
* If the weekdaynum string is invalid.
*/
public static WeekdayNum valueOf(String value, boolean tolerant) throws InvalidRecurrenceRuleException
{
try
{
int len = value.length();
if (len > 2)
{
// includes a position
int pos = Integer.parseInt(value.substring(value.charAt(0) == '+' ? 1 : 0, len - 2));
if (!tolerant && (pos == 0 || pos < -53 || pos > 53))
{
throw new InvalidRecurrenceRuleException("invalid weeknum: '" + value + "'");
}
return new WeekdayNum(pos, Weekday.valueOf(value.substring(len - 2)));
}
else
{
return new WeekdayNum(0, Weekday.valueOf(value));
}
}
catch (Exception e)
{
throw new InvalidRecurrenceRuleException("invalid weeknum: '" + value + "'", e);
}
}
/**
* Parse a weekdaynum String as defined in RFC 5545 (this definition equals the
* definition in RFC 2445). In contrast to {@link #valueOf(String, boolean)} this method is always strict and throws on every invalid value.
*
* @param value
* The weekdaynum String to parse.
*
* @return A new {@link WeekdayNum} instance.
*
* @throws InvalidRecurrenceRuleException
* If the weekdaynum string is invalid.
*/
public static WeekdayNum valueOf(String value) throws InvalidRecurrenceRuleException
{
return valueOf(value, false);
}
@Override
public String toString()
{
return pos == 0 ? weekday.name() : Integer.valueOf(pos) + weekday.name();
}
}
/**
* Type safe and null
safe way to test an object for equality with 1.
*
* This one works even if other
is null
or not an integer.
*
*
* if (ONE.equals(other)) ...
*
*
* This one fails if other is not an integer.
*
*
* if (other == 1)
*
*/
private final static Integer ONE = 1;
/**
* Pre-built "FREQ=" string, used for validation in RFC2445_STRICT mode.
*/
private final static String FREQ_PREFIX = Part.FREQ.name() + "=";
/**
* The default calendar scale - Gregorian Calendar.
*/
private final static CalendarMetrics DEFAULT_CALENDAR_SCALE = new GregorianCalendarMetrics(Weekday.MO, 4);
/**
* {@link Part}s that don't provide expander or limiter.
*/
private final static Set NON_EXPANDABLE = EnumSet.of(Part.FREQ, Part.INTERVAL, Part.WKST, Part.RSCALE);
/**
* The default skip value if RSCALE is present but SKIP is not.
*/
private final static Skip SKIP_DEFAULT = Skip.OMIT;
/**
* The parser mode. This can not be changed once the rule has been created.
*/
public final RfcMode mode;
/**
* The parts of this rule.
*/
private EnumMap mParts = new EnumMap<>(Part.class);
/**
* A map of x-parts. This is only used in RFC 2445 modes, RFC 5554 doesn't support X-parts.
*/
private Map mXParts = null;
/**
* The current calendar scale.
*/
private CalendarMetrics mCalendarMetrics = DEFAULT_CALENDAR_SCALE;
/**
* Create a new recurrence rule from String using the {@link RfcMode} {@link RfcMode#RFC5545_LAX}. The parser will be quite tolerant and skip any invalid
* parts to produce a valid recurrence rule.
*
* @param recur
* A recurrence rule string as defined in RFC 5545.
*
* @throws InvalidRecurrenceRuleException
* If an unrecoverable error occurs when parsing the rule (like FREQ is missing, or mutually exclusive parts have been found).
*/
public RecurrenceRule(String recur) throws InvalidRecurrenceRuleException
{
this(recur, RfcMode.RFC5545_LAX);
}
/**
* Create a new recurrence rule from String using a custom {@link RfcMode}.
*
* @param recur
* A recurrence rule string as defined in RFC 5545.
* @param mode
* A {@link RfcMode} to change the parsing behaviour in case of errors.
*
* @throws InvalidRecurrenceRuleException
* If the rule is invalid with respect to the chosen mode or if an unrecoverable error occurs when parsing the rule (like FREQ is missing, or
* mutually exclusive parts have been found).
*/
public RecurrenceRule(String recur, RfcMode mode) throws InvalidRecurrenceRuleException
{
this.mode = mode;
parseString(recur);
}
/**
* Create a new recurrence rule with the given base frequency. This constructor will use {@link RfcMode#RFC5545_STRICT}, so created rules will have to
* comply with RFC 5545, otherwise an exception is thrown.
*
* @param freq
* The {@link Freq} values that specified the base frequency for this rule.
*/
public RecurrenceRule(Freq freq)
{
this(freq, RfcMode.RFC5545_STRICT);
}
/**
* Create a new recurrence rule with the given base frequency using a custom {@link RfcMode}.
*
* @param freq
* The {@link Freq} values that specified the base frequency for this rule.
* @param mode
* A {@link RfcMode} to change the behavior in case of errors.
*/
public RecurrenceRule(Freq freq, RfcMode mode)
{
this.mode = mode;
mParts.put(Part.FREQ, freq);
}
/**
* Parse the given recurrence rule and populate {@link #mParts}. This method is tolerant in a way that it just drops invalid parts not allowed in the
* current {@link RfcMode}. Also, it doesn't require FREQ to be the first part (that's required in RFC
* 2445 but not in RFC 5545).
*
* @param recur
* A recurrence rule string.
*/
private void parseString(String recur) throws InvalidRecurrenceRuleException
{
if (recur == null)
{
// definitely invalid!
throw new IllegalArgumentException("recur must not be null");
}
boolean relaxed = mode == RfcMode.RFC2445_LAX || mode == RfcMode.RFC5545_LAX;
if (relaxed)
{
// remove any spaces in LAX modes
recur = recur.trim();
}
// RRULEs are case-agnostic so convert everything to upper-case
recur = recur.toUpperCase(Locale.ENGLISH);
String[] parts = recur.split(";");
if (mode == RfcMode.RFC2445_STRICT && !parts[0].startsWith(FREQ_PREFIX))
{
// in RFC2445 rules must start with "FREQ=" !
throw new InvalidRecurrenceRuleException(
"RFC 2445 requires FREQ to be the first part of the rule: " + recur);
}
CalendarMetrics calScale = mCalendarMetrics;
CalendarMetrics rScale = DEFAULT_CALENDAR_SCALE;
EnumMap partMap = mParts;
// Map partMap = new HashMap(12);
String rscaleKey = Part.RSCALE.name();
// find RSCALE first, we need it to parse some of the other parts properly
for (String keyvalue : parts)
{
if (keyvalue.startsWith(rscaleKey))
{
int equals = keyvalue.indexOf("=");
if (equals > 0)
{
String key = keyvalue.substring(0, equals);
if (key.equals(rscaleKey))
{
String value = keyvalue.substring(equals + 1);
rScale = (CalendarMetrics) Part.RSCALE.converter.parse(value, calScale, null /* that's what we're trying to find out */,
relaxed);
partMap.put(Part.RSCALE, rScale);
break;
}
}
else if (!relaxed)
{
// strict modes throw on empty parts
throw new InvalidRecurrenceRuleException("Missing '=' in part '" + keyvalue + "'");
}
}
}
// parse all other parts now
for (String keyvalue : parts)
{
int equals = keyvalue.indexOf("=");
if (equals > 0)
{
String key = keyvalue.substring(0, equals);
String value = keyvalue.substring(equals + 1);
Part part;
try
{
part = Part.valueOf(key);
}
catch (IllegalArgumentException e)
{
// X-Parts are rarely used, so we only handle them if no other part matched. That ensures we parse the average case as fast as possible.
if (key.length() > 2 && key.charAt(0) == 'X' && key.charAt(1) == '-')
{
// this is an X-Part
switch (mode)
{
case RFC2445_LAX:
case RFC2445_STRICT:
setXPart(key, value);
break;
case RFC5545_LAX:
// ignore x-parts
continue;
case RFC5545_STRICT:
throw new InvalidRecurrenceRuleException("invalid part " + key + " in " + recur);
}
}
else if (!relaxed)
{
throw new InvalidRecurrenceRuleException("invalid part " + key + " in " + recur);
}
continue;
}
if (part == Part.RSCALE)
{
// we have already parsed RSCALE
continue;
}
if (!relaxed && partMap.containsKey(part))
{
// strict modes don't allow duplicate parts
throw new InvalidRecurrenceRuleException("duplicate part " + part + " in " + recur);
}
try
{
Object partValue = part.converter.parse(value, calScale, rScale, relaxed);
if (partValue != null && (part != Part.INTERVAL || !ONE.equals(
partValue) /* do not store intervals with value 1 */))
{
partMap.put(part, partValue);
}
}
catch (InvalidRecurrenceRuleException e)
{
if (!relaxed)
{
throw e;
}
else
{
// just skip invalid parts in lax modes
}
}
}
else if (!relaxed)
{
// strict modes throw on empty parts
throw new InvalidRecurrenceRuleException("Missing '=' in part '" + keyvalue + "'");
}
}
if (partMap.containsKey(Part.RSCALE) && !partMap.containsKey(Part.SKIP))
{
// ensure the default value is present
partMap.put(Part.SKIP, SKIP_DEFAULT);
}
if (getSkip() != Skip.OMIT)
{
switch (getFreq())
{
case YEARLY:
// add skip filter to handle leap months
mParts.put(Part._BYMONTHSKIP, null);
// fall through to also add the _BYMONTHDAYSKIP filter
case MONTHLY:
// add skip filter to handle leap days
mParts.put(Part._BYMONTHDAYSKIP, null);
break;
default:
break;
}
}
// validate the rule
validate();
}
/**
* Checks for invalid rules when a numeric value is set in BYDAY. Depending on the mode either an exception is thrown or the BYDAY rule is simply dropped.
*
* @param freq
* The {@link Freq} specified in the rule.
*
* @throws InvalidRecurrenceRuleException
* if the mode is set to RFC5545_STRICT and an invalid rule is detected.
*/
private void checkForInvalidNumericInByDay(Freq freq) throws InvalidRecurrenceRuleException
{
EnumMap partMap = mParts;
if (partMap.containsKey(Part.BYDAY))
{
@SuppressWarnings("unchecked")
List values = (ArrayList) partMap.get(Part.BYDAY);
for (WeekdayNum value : values)
{
if (value.pos != 0) // user specified integer in BYDAY rule
{
/**
* https://tools.ietf.org/html/rfc5545#section-3.3.10
* "The BYDAY rule part MUST NOT be specified with a numeric value when the FREQ rule part is not set to MONTHLY or YEARLY."
*/
if (freq != Freq.YEARLY && freq != Freq.MONTHLY)
{
if (mode == RfcMode.RFC5545_STRICT)
{
final String errMsg = "The BYDAY rule part must not be specified with a numeric value when the FREQ "
+ "rule part is not set to MONTHLY or YEARLY.";
throw new InvalidRecurrenceRuleException(errMsg);
}
else
{
partMap.remove(Part.BYDAY);
}
}
/**
* https://tools.ietf.org/html/rfc5545#section-3.3.10
* "Furthermore, the BYDAY rule part MUST NOT be specified with a numeric value with the FREQ rule part set to YEARLY when the BYWEEKNO rule part is specified."
*/
else if (freq == Freq.YEARLY && partMap.containsKey(Part.BYWEEKNO))
{
if (mode == RfcMode.RFC5545_STRICT)
{
final String errMsg = "The BYDAY rule part must not be specified with a numeric value with"
+ " the FREQ rule part set to YEARLY when BYWEEKNO is set";
throw new InvalidRecurrenceRuleException(errMsg);
}
else
{
partMap.remove(Part.BYDAY);
}
}
}
}
}
}
/**
* Validate this rule.
*
* @throws InvalidRecurrenceRuleException
* if the rule is not valid with respect to the current {@link #mode}.
*/
private void validate() throws InvalidRecurrenceRuleException
{
EnumMap partMap = mParts;
Freq freq = (Freq) partMap.get(Part.FREQ);
// FREQ is mandatory part of each rule
if (freq == null)
{
throw new InvalidRecurrenceRuleException("FREQ part is missing");
}
final boolean strict = mode == RfcMode.RFC2445_STRICT || mode == RfcMode.RFC5545_STRICT;
// UNTIL and COUNT are mutually exclusive
if (partMap.containsKey(Part.UNTIL) && partMap.containsKey(Part.COUNT))
{
throw new InvalidRecurrenceRuleException("UNTIL and COUNT must not occur in the same rule.");
}
// interval must not be 0 or less
if (getInterval() <= 0)
{
if (strict)
{
throw new InvalidRecurrenceRuleException("INTERVAL must not be <= 0");
}
else
{
// just remove interval and assume 1
partMap.remove(Part.INTERVAL);
}
}
if (freq != Freq.YEARLY && partMap.containsKey(Part.BYWEEKNO))
{
if (strict)
{
throw new InvalidRecurrenceRuleException("BYWEEKNO is allowed in YEARLY rules only");
}
else
{
partMap.put(Part.FREQ, Freq.YEARLY);
}
}
if (mode == RfcMode.RFC5545_STRICT)
{
// in RFC 5545 BYYEARDAY does not support DAILY, WEEKLY and MONTHLY rules
if ((freq == Freq.DAILY || freq == Freq.WEEKLY || freq == Freq.MONTHLY) && partMap.containsKey(
Part.BYYEARDAY))
{
throw new InvalidRecurrenceRuleException(
"In RFC 5545, BYYEARDAY is not allowed in DAILY, WEEKLY or MONTHLY rules");
}
// in RFC 5545 BYMONTHAY must not be used in WEEKLY rules
if (freq == Freq.WEEKLY && partMap.containsKey(Part.BYMONTHDAY))
{
throw new InvalidRecurrenceRuleException("In RFC 5545, BYMONTHDAY is not allowed in WEEKLY rules");
}
}
/**
* BYSETPOS is only valid in combination with another BYxxx rule. We therefore check the number of elements. If this number is larger than cnt the rule
* contains another BYxxx rule and is therefore valid.
*/
if (partMap.containsKey(Part.BYSETPOS))
{
if (!partMap.containsKey(Part.BYDAY) && !partMap.containsKey(Part.BYMONTHDAY) && !partMap.containsKey(
Part.BYMONTH)
&& !partMap.containsKey(Part.BYHOUR) && !partMap.containsKey(Part.BYMINUTE) && !partMap.containsKey(
Part.BYSECOND)
&& !partMap.containsKey(Part.BYWEEKNO) && !partMap.containsKey(Part.BYYEARDAY))
{
if (strict)
{
// we're in strict mode => throw exception
throw new InvalidRecurrenceRuleException(
"BYSETPOS must only be used in conjunction with another BYxxx rule.");
}
else
{
// we're in lax mode => drop BYSETPOS
partMap.remove(Part.BYSETPOS);
}
}
}
/**
* Check for invalid rules when a numeric value is set in BYDAY.
*/
checkForInvalidNumericInByDay(freq);
}
/**
* Validate if adding a specific list part would result in a valid rule.
*
* @param part
* The part to be added.
* @param value
* The value of this part.
*
* @throws InvalidRecurrenceRuleException
* if the rule is not valid with respect to the current {@link #mode}.
*/
private void validate(Part part, List value) throws InvalidRecurrenceRuleException
{
Freq freq = (Freq) mParts.get(Part.FREQ);
if (mode == RfcMode.RFC5545_STRICT)
{
// in RFC 5545 BYWEEKNO can be used with YEARLY rules only
if (freq != Freq.YEARLY && part == Part.BYWEEKNO)
{
throw new InvalidRecurrenceRuleException("In RFC 5545, BYWEEKNO is allowed in YEARLY rules only");
}
// in RFC 5545 BYYEARDAY does not support DAILY, WEEKLY and MONTHLY rules
if ((freq == Freq.DAILY || freq == Freq.WEEKLY || freq == Freq.MONTHLY) && part == Part.BYYEARDAY)
{
throw new InvalidRecurrenceRuleException(
"In RFC 5545, BYYEARDAY is not allowed in DAILY, WEEKLY or MONTHLY rules");
}
// in RFC 5545 BYMONTHAY must not be used in WEEKLY rules
if (freq == Freq.WEEKLY && part == Part.BYMONTHDAY)
{
throw new InvalidRecurrenceRuleException("In RFC 5545, BYMONTHDAY is not allowed in WEEKLY rules");
}
}
}
/**
* Return the base frequency of this recurrence rule.
*
* @return The {@link Freq} value of this rule.
*/
public Freq getFreq()
{
return (Freq) mParts.get(Part.FREQ);
}
/**
* Set the base frequency of this recurrence rule. TODO: check if the rule is still valid afterwards (honor the silent parameter)
*
* @param freq
* The new {@link Freq} value of this rule.
* @param silent
* true
to drop {@link Part}s that are no longer valid with the new frequency silently, false
to throw an exception in
* that case.
*/
public void setFreq(Freq freq, boolean silent)
{
mParts.put(Part.FREQ, freq);
if (mode == RfcMode.RFC5545_STRICT || mode == RfcMode.RFC5545_LAX)
{
// me might end up with an invalid rule when changing the base frequency.
}
}
/**
* Return value of the skip part of this rule.
*
* @return The {@link Skip} value of this rule.
*/
public Skip getSkip()
{
Skip skip = (Skip) mParts.get(Part.SKIP);
return skip == null ? Skip.OMIT : skip;
}
/**
* Set the skip part of this recurrence rule. Consider to set a {@link Part#RSCALE} value when setting a SKIP rule, otherwise it will default to GREGORIAN
* calendar.
*
* @param skip
* The new {@link Skip} value of this rule, null
and {@link Skip#OMIT} will remove the SKIP part, the later one is the default anyway.
*/
public void setSkip(Skip skip)
{
if (skip == null || skip == Skip.OMIT)
{
mParts.remove(Part.SKIP);
// remove filters as well
mParts.remove(Part._BYMONTHSKIP);
mParts.remove(Part._BYMONTHDAYSKIP);
}
else
{
mParts.put(Part.SKIP, skip);
if (!mParts.containsKey(Part.RSCALE))
{
mParts.put(Part.RSCALE, DEFAULT_CALENDAR_SCALE);
}
Freq freq = getFreq();
if (freq == Freq.YEARLY || freq == Freq.MONTHLY)
{
// this rule needs skip filters
mParts.put(Part._BYMONTHSKIP, null);
mParts.put(Part._BYMONTHDAYSKIP, null);
}
}
}
/**
* Get the INTERVAL of this rule.
*
* @return The INTERVAL of this rule or 1
if no INTERVAL has been specified.
*/
public int getInterval()
{
Integer interval = (Integer) mParts.get(Part.INTERVAL);
// return the default value of 1 if no interval is given
return interval == null ? 1 : interval;
}
/**
* Set the INTERVAL of this rule. The interval must be a positive integer. A value of 1
will just remove the INTERVAL part since that's the
* default value anyway.
*
* @param interval
* The new interval of this rule.
*
* @throws IllegalArgumentException
* if interval is not a positive integer value.
*/
public void setInterval(int interval)
{
if (interval > 1)
{
mParts.put(Part.INTERVAL, interval);
}
else if (interval <= 0)
{
throw new IllegalArgumentException("Interval must be a positive integer value");
}
else
{
// interval == 1, since that's the default we just remove it
mParts.remove(Part.INTERVAL);
}
}
/**
* Get the last date an instance my have. If the rule has an UNTIL part the result is a {@link DateTime} set to the correct time. The time zone is either
* UTC or floating.
*
* @return A {@link DateTime} set to the UNTIL value if an UNTIL part is present, null
otherwise.
*/
public DateTime getUntil()
{
return (DateTime) mParts.get(Part.UNTIL);
}
/**
* Set the latest possible date of an instance. This will remove any COUNT rule if present. If the time zone of until
is not UTC and until is
* not floating it's automatically converted to UTC.
*
* @param until
* The UNTIL part of this rule or null
to let the instances recur forever.
*/
public void setUntil(DateTime until)
{
if (until == null)
{
mParts.remove(Part.UNTIL);
mParts.remove(Part.COUNT);
}
else
{
if ((!until.isFloating() && !DateTime.UTC.equals(until.getTimeZone())) || !mCalendarMetrics.equals(
until.getCalendarMetrics()))
{
mParts.put(Part.UNTIL, new DateTime(mCalendarMetrics, DateTime.UTC, until.getTimestamp()));
}
else
{
mParts.put(Part.UNTIL, until);
}
mParts.remove(Part.COUNT);
}
}
/**
* Get the number if instances in the recurrence set. If this rule has no COUNT limit this will return null
*
* @return The number of instances or null
.
*/
public Integer getCount()
{
return (Integer) mParts.get(Part.COUNT);
}
/**
* Set the number of instances in the recurrence set. This will remove any UNTIL rule if present.
*
* @param count
* The number if instances.
*/
public void setCount(int count)
{
mParts.put(Part.COUNT, count);
mParts.remove(Part.UNTIL);
}
/**
* Returns whether this recurrence rule recurs forever.
*
* @return true
if this rule contains neither an {@link Part#UNTIL} nor a {@link Part#COUNT} part, false
otherwise.
*/
public boolean isInfinite()
{
return !mParts.containsKey(Part.UNTIL) && !mParts.containsKey(Part.COUNT);
}
/**
* Checks if a specific part is present in this rule.
*
* @param part
* The part if interest.
*
* @return true
if this rule has this part, false
otherwise
*/
public boolean hasPart(Part part)
{
return mParts.containsKey(part);
}
/**
* Returns a specific by-rule. part
may be one of {@link Part#BYSECOND}, {@link Part#BYMINUTE}, {@link Part#BYHOUR}, {@link Part#BYMONTHDAY},
* {@link Part#BYYEARDAY}, {@link Part#BYWEEKNO}, {@link Part#BYMONTH}, or {@link Part#BYSETPOS}. To get {@link Part#BYDAY} use {@link
* #getByDayPart()}.
*
*
* @param part
* The by-rule to return.
*
* @return A list of integer values.
*/
@SuppressWarnings("unchecked")
public List getByPart(Part part)
{
switch (part)
{
case BYSECOND:
case BYMINUTE:
case BYHOUR:
case BYMONTHDAY:
case BYYEARDAY:
case BYWEEKNO:
case BYMONTH:
case BYSETPOS:
return (List) mParts.get(part);
default:
throw new IllegalArgumentException(part.name() + " is not a list type");
}
}
/**
* Set a specific by-rule. part
may be one of {@link Part#BYSECOND}, {@link Part#BYMINUTE}, {@link Part#BYHOUR}, {@link Part#BYMONTHDAY},
* {@link Part#BYYEARDAY}, {@link Part#BYWEEKNO}, {@link Part#BYMONTH}, or {@link Part#BYSETPOS}. To set {@link Part#BYDAY} use {@link
* #setByDayPart(List)}.
*
* @param part
* The by-rule to set.
* @param value
* A list of integers that specify the rule or null
(or an empty list) to remove the part.
*
* @throws InvalidRecurrenceRuleException
* if the list would become invalid by adding this part (this respects the current {@link RfcMode}.
*/
public void setByPart(Part part, List value) throws InvalidRecurrenceRuleException
{
if (value == null || value.size() == 0)
{
mParts.remove(part);
}
else
{
switch (part)
{
case BYSECOND:
case BYMINUTE:
case BYHOUR:
case BYMONTHDAY:
case BYYEARDAY:
case BYWEEKNO:
case BYMONTH:
case BYSETPOS:
validate(part, value);
mParts.put(part, value);
break;
default:
throw new IllegalArgumentException(part.name() + " is not a list type");
}
}
}
/**
* Set a specific by-rule. part
may be one of {@link Part#BYSECOND}, {@link Part#BYMINUTE}, {@link Part#BYHOUR}, {@link Part#BYMONTHDAY},
* {@link Part#BYYEARDAY}, {@link Part#BYWEEKNO}, {@link Part#BYMONTH}, or {@link Part#BYSETPOS}. To set {@link Part#BYDAY} use {@link
* #setByDayPart(List)}.
*
* @param part
* The by-rule to set.
* @param values
* Integers that specify the rule or null
(or an empty list) to remove the part.
*
* @throws InvalidRecurrenceRuleException
* if the list would become invalid by adding this part (this respects the current {@link RfcMode}.
*/
public void setByPart(Part part, Integer... values) throws InvalidRecurrenceRuleException
{
if (values == null || values.length == 0)
{
mParts.remove(part);
}
else
{
setByPart(part, Arrays.asList(values));
}
}
/**
* Set the BYDAY part of this rule.
*
* @param value
* A {@link List} of {@link WeekdayNum}s or null
or an empty List to remove the part
*/
public void setByDayPart(List value)
{
if (value == null || value.size() == 0)
{
mParts.remove(Part.BYDAY);
}
else
{
mParts.put(Part.BYDAY, value);
}
}
/**
* Return the value of the BYDAY part of the rule if there is any.
*
* @return A {@link List} of {@link WeekdayNum}s if the part is present or null
if there is no such part.
*/
@SuppressWarnings("unchecked")
public List getByDayPart()
{
return (List) mParts.get(Part.BYDAY);
}
/**
* Get the start of the week as defined in the rule. If no WKST part is set this method will return the default value, which is {@link Weekday#MO}.
*
* @return A {@link Weekday}.
*/
public Weekday getWeekStart()
{
Weekday wkst = (Weekday) mParts.get(Part.WKST);
return wkst == null ? Weekday.MO /* weeks start with Monday by default */ : wkst;
}
/**
* Set the start of the week. If the start is set to {@link Weekday#MO} the WKST part is effectively removed, since that's the default value. This value is
* important for rules having a BYWEEKNO or BYDAY part.
*
* @param wkst
* The start of the week to use when calculating the instances.
*/
public void setWeekStart(Weekday wkst)
{
setWeekStart(wkst, false);
}
/**
* Set the start of the week. If the start is set to {@link Weekday#MO} the WKST part is effectively removed (unless keepWkStMo == true
), since
* that's the default value. This value is important for rules having a BYWEEKNO or BYDAY part.
*
* @param wkst
* The start of the week to use when calculating the instances.
* @param keepWkStMo
* set to true
to keep the WKST field if the value is {@link Weekday#MO}. Since Monday is the default adding it is not necessary, but
* some implementations might be broken and use a different weekstart if it's not explicitly specified.
*/
public void setWeekStart(Weekday wkst, boolean keepWkStMo)
{
if (wkst == Weekday.MO && !keepWkStMo)
{
// Monday is the default, so just remove the part
mParts.remove(Part.WKST);
}
else
{
mParts.put(Part.WKST, wkst);
}
}
/**
* Sets an x-part. x-parts are supported by RFC 2445 only. If {@link #mode} is set to {@link RfcMode#RFC5545_LAX} a call to this method will do nothing. If
* {@link #mode} is set to {@link RfcMode#RFC5545_STRICT} this method will throw an {@link UnsupportedOperationException}. Note that calling this method
* in RFC 2445 mode will override any existing x-part of the same name.
*
* @param xname
* The name of the x-part. Must be a valid identifier.
* @param value
* The value of the x-part. Must be a valid name.
*
* @throws UnsupportedOperationException
* if {@link #mode} is set to {@link RfcMode#RFC5545_STRICT}.
*/
public void setXPart(String xname, String value)
{
if (mode == RfcMode.RFC5545_STRICT)
{
throw new UnsupportedOperationException("x-parts are not supported by RFC5545.");
}
if ((value == null && mXParts == null) || xname == null || mode == RfcMode.RFC5545_LAX)
{
return;
}
if (value == null)
{
if (mXParts.remove(xname) == null)
{
mXParts.remove(xname.toUpperCase(Locale.ENGLISH));
}
}
else
{
if (xname.length() <= 2 || (xname.charAt(0) != 'X' && xname.charAt(0) != 'x') || xname.charAt(1) != '-')
{
throw new IllegalArgumentException("invalid x-name: '" + xname + "'");
}
if (mXParts == null)
{
mXParts = new HashMap(8);
}
// TODO: validate xname and value
mXParts.put(xname.toUpperCase(Locale.ENGLISH), value);
}
}
/**
* Returns whether a specific x-part is present in the rule. Since RFC 5545 doesn't support x-parts this method will always return false
if
* {@link #mode} equals {@link RfcMode#RFC5545_LAX} or {@link RfcMode#RFC5545_STRICT}.
*
* @param xname
* The name of the x-part to check for.
*
* @return true
if the part is present, false
otherwise.
*/
public boolean hasXPart(String xname)
{
if (xname == null || mXParts == null || mode == RfcMode.RFC5545_LAX || mode == RfcMode.RFC5545_STRICT)
{
return false;
}
return mXParts.containsKey(xname) || mXParts.containsKey(xname.toUpperCase(Locale.ENGLISH));
}
/**
* Returns a specific x-part. Since RFC 5545 doesn't support x-parts this method will always return null
if {@link #mode} equals {@link
* RfcMode#RFC5545_LAX} or {@link RfcMode#RFC5545_STRICT}.
*
* @param xname
* The name of the x-part to return.
*
* @return The value of the x-part or null
.
*/
public String getXPart(String xname)
{
if (xname == null || mXParts == null || mode == RfcMode.RFC5545_LAX || mode == RfcMode.RFC5545_STRICT)
{
return null;
}
String result = mXParts.get(xname);
return result != null ? result : mXParts.get(xname.toUpperCase(Locale.ENGLISH));
}
/**
* Get a new {@link RuleIterator} that iterates all instances of this rule. Note: If the rule contains an UNTIL part with a floating
* value, you have to provide null
as the timezone.
*
* @param start
* The time of the first instance in milliseconds since the epoch.
* @param timezone
* The {@link TimeZone} of the first instance or null
for floating times.
*
* @return A {@link RecurrenceRuleIterator}.
*/
public RecurrenceRuleIterator iterator(long start, TimeZone timezone)
{
// TODO: avoid creating a temporary DATETIME instance.
DateTime dt = new DateTime(mCalendarMetrics, timezone, start);
DateTime until = getUntil();
if (until != null && until.isAllDay())
{
dt = dt.toAllDay();
}
return iterator(dt);
}
/**
* Get a new {@link RuleIterator} that iterates all instances of this rule. Note: if an UNTIL part is present and it's value is a
* floating time then start must be floating as well and vice versa. The same applies if the UNTIL value is an all-day value
*
* @param start
* The first instance.
*
* @return A {@link RuleIterator}.
*/
public RecurrenceRuleIterator iterator(DateTime start)
{
DateTime until = getUntil();
if (until != null)
{
if (!mode.mIsLax && until.isAllDay() != start.isAllDay())
{
throw new IllegalArgumentException(
"using allday start times with non-allday until values (and vice versa) is not allowed in strict modes");
}
if (until.isFloating() != start.isFloating())
{
throw new IllegalArgumentException(
"using floating start times with absolute until values (and vice versa) is not allowed");
}
}
CalendarMetrics rScaleCalendarMetrics = (CalendarMetrics) mParts.get(Part.RSCALE);
if (rScaleCalendarMetrics == null)
{
rScaleCalendarMetrics = new GregorianCalendarMetrics(getWeekStart(), 4);
}
// make sure we convert start to the rscale calendar metrics if they don't equal
long startInstance = !rScaleCalendarMetrics.scaleEquals(start.getCalendarMetrics())
? new DateTime(rScaleCalendarMetrics, start).getInstance()
: start.getInstance();
TimeZone startTimeZone = start.isFloating() ? null : start.getTimeZone();
RuleIterator iterator = Part.FREQ.getExpander(this, null, rScaleCalendarMetrics, startInstance, startTimeZone);
// add SanityFilter if not present yet
mParts.put(Part._SANITY_FILTER, null);
Set parts = EnumSet.copyOf(mParts.keySet());
if (getFreq() == Freq.YEARLY)
{
Set rewritableParts = EnumSet.copyOf(parts);
rewritableParts.retainAll(REWRITE_PARTS);
if (YEAR_REWRITE_MAP.containsKey(rewritableParts))
{
parts.removeAll(rewritableParts);
parts.addAll(YEAR_REWRITE_MAP.get(rewritableParts));
}
}
parts.removeAll(NON_EXPANDABLE);
for (Part p : parts)
{
// add a filter for each rule part
if (p.expands(this))
{
// if a part returns null for the expander just skip it
RuleIterator newIterator = p.getExpander(this, iterator, rScaleCalendarMetrics, startInstance, startTimeZone);
iterator = newIterator == null ? iterator : newIterator;
}
else
{
((ByExpander) iterator).addFilter(p.getFilter(this, rScaleCalendarMetrics));
}
}
return new RecurrenceRuleIterator(iterator, start, rScaleCalendarMetrics);
}
@Override
public String toString()
{
// the average rule is not longer than 100 characters, we add some buffer to avoid a copy operation
StringBuilder result = new StringBuilder(160);
boolean first = true;
CalendarMetrics rscale = (CalendarMetrics) mParts.get(Part.RSCALE);
if (rscale == null)
{
rscale = DEFAULT_CALENDAR_SCALE;
}
// just write all parts separated by semicolon to the result string
// the order of the parts guarantees that FREQ is always the first part (as required by RFC 2445)
for (Part part : Part.values())
{
if (part == Part._BYMONTHDAYSKIP || part == Part._BYMONTHSKIP || part == Part._SANITY_FILTER)
{
// don't render these two
continue;
}
Object value = mParts.get(part);
if (value != null)
{
if (first)
{
first = false;
}
else
{
result.append(";");
}
result.append(part.name());
result.append("=");
part.converter.serialize(result, value, rscale);
}
}
if ((mode == RfcMode.RFC2445_LAX || mode == RfcMode.RFC2445_STRICT) && mXParts != null && mXParts.size() != 0)
{
// serialize x-parts
for (Entry part : mXParts.entrySet())
{
result.append(";");
result.append(part.getKey());
result.append("=");
result.append(part.getValue());
}
}
return result.toString();
}
/**
* Abstract class to parse and serialize a specific part of a RRULE.
*
* @param
* The type returned by the parser and expected by the serializer.
*
* @author Marten Gajda
*/
private static abstract class ValueConverter
{
/**
* Parses a string for a specific value .
*
* @param value
* The string representation of the value.
* @param tolerant
* true
to ignore any errors if possible
*
* @return An instance of with the correct value.
*
* @throws InvalidRecurrenceRuleException
* if the value is invalid.
*/
public abstract T parse(String value, CalendarMetrics calScale, CalendarMetrics rScale, boolean tolerant) throws InvalidRecurrenceRuleException;
/**
* Write the string representation of a value of type to a {@link StringBuilder}. The default implementation just calls {@link #toString()} on
* the value.
*
* @param out
* The {@link StringBuilder} to write to.
* @param value
* The value to serialize.
*/
public void serialize(StringBuilder out, Object value, CalendarMetrics rScale)
{
out.append(value.toString());
}
}
/**
* Generic converter for comma separated list values.
*
* @param
* The type of the list elements.
*
* @author Marten Gajda
*/
private static class ListValueConverter extends ValueConverter>
{
private final ValueConverter mElementConverter;
public ListValueConverter(ValueConverter elementConverter)
{
mElementConverter = elementConverter;
}
@Override
public Collection parse(String value, CalendarMetrics calScale, CalendarMetrics rScale, boolean tolerant) throws InvalidRecurrenceRuleException
{
List result = new ArrayList(32);
String[] values = value.split(",");
for (String val : values)
{
try
{
result.add(mElementConverter.parse(val, calScale, rScale, tolerant));
}
catch (InvalidRecurrenceRuleException e)
{
if (!tolerant)
{
throw e;
}
}
catch (Exception e)
{
if (!tolerant)
{
throw new InvalidRecurrenceRuleException("could not parse list '" + value + "'", e);
}
}
}
if (result.size() > 0)
{
return result;
}
else
{
throw new InvalidRecurrenceRuleException("empty lists are not allowed");
}
}
@Override
public void serialize(StringBuilder out, Object value, CalendarMetrics rScale)
{
boolean first = true;
for (Object v : (Collection>) value)
{
if (first)
{
first = false;
}
else
{
out.append(",");
}
mElementConverter.serialize(out, v, rScale);
}
}
}
/**
* A {@link ValueConverter} for month values.
*
* @author Marten Gajda
*/
private static class MonthConverter extends ValueConverter
{
@Override
public Integer parse(String value, CalendarMetrics calScale, CalendarMetrics rScale, boolean tolerant) throws InvalidRecurrenceRuleException
{
return rScale.packedMonth(value);
}
@Override
public void serialize(StringBuilder out, Object value, CalendarMetrics rScale)
{
out.append(rScale.packedMonthToString((Integer) value));
}
}
/**
* A converter for integer list values.
*
* @author Marten Gajda
*/
private static class IntegerConverter extends ValueConverter
{
private final int mMinValue;
private final int mMaxValue;
private boolean mNoZero = false;
/**
* Creates a new converter for integer lists.
*
* @param min
* The lowest allowed value.
* @param max
* The highest allowed value.
*/
public IntegerConverter(int min, int max)
{
mMaxValue = max;
mMinValue = min;
}
/**
* Disallow the value 0
*
* @return This instance.
*/
public IntegerConverter noZero()
{
mNoZero = true;
return this;
}
@Override
public Integer parse(String value, CalendarMetrics calScale, CalendarMetrics rScale, boolean tolerant) throws InvalidRecurrenceRuleException
{
try
{
int val = Integer.parseInt(value);
if (val < mMinValue || val > mMaxValue || mNoZero && val == 0)
{
throw new InvalidRecurrenceRuleException("int value out of range: " + val);
}
return val;
}
catch (NumberFormatException e)
{
throw new InvalidRecurrenceRuleException("illegal int value: " + value);
}
}
}
/**
* Converts a list of {@link WeekdayNum} values.
*
* @author Marten Gajda
*/
private static class WeekdayNumConverter extends ValueConverter
{
@Override
public WeekdayNum parse(String value, CalendarMetrics calScale, CalendarMetrics rScale, boolean tolerant) throws InvalidRecurrenceRuleException
{
return WeekdayNum.valueOf(value, tolerant);
}
}
/**
* Converts a {@link Weekday} value.
*
* @author Marten Gajda
*/
private static class WeekdayConverter extends ValueConverter
{
@Override
public Weekday parse(String value, CalendarMetrics calScale, CalendarMetrics rScale, boolean tolerant) throws InvalidRecurrenceRuleException
{
try
{
return Weekday.valueOf(value);
}
catch (IllegalArgumentException e)
{
throw new InvalidRecurrenceRuleException("illegal weekday: " + value);
}
}
}
/**
* Converts the value of the FREQ part from/to a {@link Freq} instance.
*
* @author Marten Gajda
*/
private static class FreqConverter extends ValueConverter
{
@Override
public Freq parse(String value, CalendarMetrics calScale, CalendarMetrics rScale, boolean tolerant) throws InvalidRecurrenceRuleException
{
try
{
return Freq.valueOf(value);
}
catch (IllegalArgumentException e)
{
throw new InvalidRecurrenceRuleException("Unknown FREQ value " + value);
}
}
}
/**
* Converts the date-time value of an UNTIL part from/to a {@link DateTime} instance.
*
* @author Marten Gajda
*/
private static class DateTimeConverter extends ValueConverter
{
@Override
public DateTime parse(String value, CalendarMetrics calScale, CalendarMetrics rScale, boolean tolerant) throws InvalidRecurrenceRuleException
{
DateTime result = null;
try
{
result = DateTime.parse(calScale, (TimeZone) null, value);
// convert the rule to RSCALE, since we do all calculations in this scale
return calScale.scaleEquals(rScale) ? result : new DateTime(rScale, result);
}
catch (Exception e)
{
// some broken clients created UNTIL dates that end with "ZZ" - check that in tolerant mode
if (tolerant && value != null && value.endsWith("ZZ"))
{
try
{
result = DateTime.parse(calScale, null, value.substring(0, value.length() - 1));
// convert the rule to RSCALE, since we do all calculations in this scale
return calScale.scaleEquals(rScale) ? result : new DateTime(rScale, result);
}
catch (Exception e2)
{
// just fall through
}
}
throw new InvalidRecurrenceRuleException("Invalid UNTIL date: " + value, e);
}
}
}
/**
* Converts the value of the RSCALE part to a {@link CalendarMetrics} value.
*
* @author Marten Gajda
*/
private static class RScaleConverter extends ValueConverter
{
@Override
public CalendarMetrics parse(String value, CalendarMetrics calScale, CalendarMetrics rScale, boolean tolerant) throws InvalidRecurrenceRuleException
{
CalendarMetricsFactory result = UnicodeCalendarScales.getCalendarMetricsForName(value);
if (result == null)
{
// can not tolerate rscales we don't know
throw new InvalidRecurrenceRuleException("unknown calendar scale '" + value + "'");
}
return result.getCalendarMetrics(calScale.weekStart);
}
}
/**
* Converts the value of the SKIP part to a {@link Skip} value.
*
* @author Marten Gajda
*/
private static class SkipValueConverter extends ValueConverter
{
@Override
public Skip parse(String value, CalendarMetrics calScale, CalendarMetrics rScale, boolean tolerant) throws InvalidRecurrenceRuleException
{
try
{
return Skip.valueOf(value);
}
catch (IllegalArgumentException e)
{
throw new InvalidRecurrenceRuleException("Unknown SKIP value " + value);
}
}
}
/**
* A static instance of a {@link ValueConverter} that throws an error when trying to parse a value. This is for parts that are only implementation helpers.
*/
private final static ValueConverter ERROR_CONVERTER = new ValueConverter()
{
public Void parse(String value, CalendarMetrics calScale, CalendarMetrics rScale, boolean tolerant) throws InvalidRecurrenceRuleException
{
throw new InvalidRecurrenceRuleException("part not allowed in an RRULE");
}
};
}