// Copyright (C) 2006 Google Inc. // // 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 com.google.ical.values; import com.google.ical.util.TimeUtils; import java.text.ParseException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * ical schema for parsing RRULE and EXRULE content lines. * * @author mikesamuel+svn@gmail.com (Mike Samuel) */ class RRuleSchema extends IcalSchema{ private static final Pattern COMMA = Pattern.compile(","); private static final Pattern SEMI = Pattern.compile(";"); private static final Pattern X_NAME_RE = Pattern.compile( "^X-", Pattern.CASE_INSENSITIVE); private static final Pattern RRULE_PARTS = Pattern.compile( "^(FREQ|UNTIL|COUNT|INTERVAL|BYSECOND|BYMINUTE|BYHOUR|BYDAY|BYMONTHDAY|" + "BYYEARDAY|BYWEEKDAY|BYWEEKNO|BYMONTH|BYSETPOS|WKST|X-[A-Z0-9\\-]+)=" + "(.*)", Pattern.CASE_INSENSITIVE); private static final Pattern NUM_DAY = Pattern.compile( "^([+\\-]?\\d\\d?)?(SU|MO|TU|WE|TH|FR|SA)$", Pattern.CASE_INSENSITIVE); ///////////////////////////////// // ICAL Object Schema ///////////////////////////////// static RRuleSchema instance() { return new RRuleSchema(); } private RRuleSchema() { super(PARAM_RULES, CONTENT_RULES, OBJECT_RULES, XFORM_RULES); } private static final Map<String, ParamRule> PARAM_RULES; private static final Map<String, ContentRule> CONTENT_RULES; private static final Map<String, ObjectRule> OBJECT_RULES; private static final Map<String, XformRule> XFORM_RULES; static { Map<String, ParamRule> paramRules = new HashMap<String, ParamRule>(); Map<String, ContentRule> contentRules = new HashMap<String, ContentRule>(); Map<String, ObjectRule> objectRules = new HashMap<String, ObjectRule>(); Map<String, XformRule> xformRules = new HashMap<String, XformRule>(); // rrule = "RRULE" rrulparam ":" recur CRLF // exrule = "EXRULE" exrparam ":" recur CRLF objectRules.put("RRULE", new ObjectRule() { public void apply( IcalSchema schema, Map<String, String> params, String content, IcalObject target) throws ParseException { schema.applyParamsSchema("rrulparam", params, target); schema.applyContentSchema("recur", content, target); } }); objectRules.put("EXRULE", new ObjectRule() { public void apply( IcalSchema schema, Map<String, String> params, String content, IcalObject target) throws ParseException { schema.applyParamsSchema("exrparam", params, target); schema.applyContentSchema("recur", content, target); } }); // rrulparam = *(";" xparam) // exrparam = *(";" xparam) ParamRule xparamsOnly = new ParamRule() { public void apply( IcalSchema schema, String name, String value, IcalObject out) throws ParseException { schema.badParam(name, value); } }; paramRules.put("rrulparam", xparamsOnly); paramRules.put("exrparam", xparamsOnly); /* * recur = "FREQ"=freq *( * * ; either UNTIL or COUNT may appear in a 'recur', * ; but UNTIL and COUNT MUST NOT occur in the same 'recur' * * ( ";" "UNTIL" "=" enddate ) / * ( ";" "COUNT" "=" 1*DIGIT ) / * * ; the rest of these keywords are optional, * ; but MUST NOT occur more than once * * ( ";" "INTERVAL" "=" 1*DIGIT ) / * ( ";" "BYSECOND" "=" byseclist ) / * ( ";" "BYMINUTE" "=" byminlist ) / * ( ";" "BYHOUR" "=" byhrlist ) / * ( ";" "BYDAY" "=" bywdaylist ) / * ( ";" "BYMONTHDAY" "=" bymodaylist ) / * ( ";" "BYYEARDAY" "=" byyrdaylist ) / * ( ";" "BYWEEKNO" "=" bywknolist ) / * ( ";" "BYMONTH" "=" bymolist ) / * ( ";" "BYSETPOS" "=" bysplist ) / * ( ";" "WKST" "=" weekday ) / * * ( ";" x-name "=" text ) * ) */ contentRules.put("recur", new ContentRule() { public void apply(IcalSchema schema, String content, IcalObject target) throws ParseException { String[] parts = SEMI.split(content); Map<String, String> partMap = new HashMap<String, String>(); for (int i = 0; i < parts.length; ++i) { String p = parts[i]; Matcher m = RRULE_PARTS.matcher(p); if (!m.matches()) { schema.badPart(p, null); } String k = m.group(1).toUpperCase(), v = m.group(2); if (partMap.containsKey(k)) { schema.dupePart(p); } partMap.put(k, v); } if (!partMap.containsKey("FREQ")) { schema.missingPart("FREQ", content); } if (partMap.containsKey("UNTIL") && partMap.containsKey("COUNT")) { schema.badPart(content, "UNTIL & COUNT are exclusive"); } for (Map.Entry<String, String> part : partMap.entrySet()) { if (X_NAME_RE.matcher(part.getKey()).matches()) { // ignore x-name content parts continue; } schema.applyContentSchema(part.getKey(), part.getValue(), target); } } }); // exdate = "EXDATE" exdtparam ":" exdtval *("," exdtval) CRLF objectRules.put("EXDATE", new ObjectRule() { public void apply( IcalSchema schema, Map<String, String> params, String content, IcalObject target) throws ParseException { schema.applyParamsSchema("exdtparam", params, target); for (String part : COMMA.split(content)) { schema.applyContentSchema("exdtval", part, target); } } }); // "FREQ"=freq *( contentRules.put("FREQ", new ContentRule() { public void apply(IcalSchema schema, String value, IcalObject target) throws ParseException { ((RRule) target).setFreq( (Frequency) schema.applyXformSchema("freq", value)); } }); // ( ";" "UNTIL" "=" enddate ) / contentRules.put("UNTIL", new ContentRule() { public void apply(IcalSchema schema, String value, IcalObject target) throws ParseException { ((RRule) target).setUntil( (DateValue) schema.applyXformSchema("enddate", value)); } }); // ( ";" "COUNT" "=" 1*DIGIT ) / contentRules.put("COUNT", new ContentRule() { public void apply(IcalSchema schema, String value, IcalObject target) throws ParseException { ((RRule) target).setCount(Integer.parseInt(value)); } }); // ( ";" "INTERVAL" "=" 1*DIGIT ) / contentRules.put("INTERVAL", new ContentRule() { public void apply(IcalSchema schema, String value, IcalObject target) throws ParseException { ((RRule) target).setInterval(Integer.parseInt(value)); } }); // ( ";" "BYSECOND" "=" byseclist ) / contentRules.put("BYSECOND", new ContentRule() { public void apply(IcalSchema schema, String value, IcalObject target) throws ParseException { ((RRule) target).setBySecond( (int[]) schema.applyXformSchema("byseclist", value)); } }); // ( ";" "BYMINUTE" "=" byminlist ) / contentRules.put("BYMINUTE", new ContentRule() { public void apply(IcalSchema schema, String value, IcalObject target) throws ParseException { ((RRule) target).setByMinute( (int[]) schema.applyXformSchema("byminlist", value)); } }); // ( ";" "BYHOUR" "=" byhrlist ) / contentRules.put("BYHOUR", new ContentRule() { public void apply(IcalSchema schema, String value, IcalObject target) throws ParseException { ((RRule) target).setByHour( (int[]) schema.applyXformSchema("byhrlist", value)); } }); // ( ";" "BYDAY" "=" bywdaylist ) / contentRules.put("BYDAY", new ContentRule() { @SuppressWarnings("unchecked") public void apply(IcalSchema schema, String value, IcalObject target) throws ParseException { ((RRule) target).setByDay( (List<WeekdayNum>) schema.applyXformSchema("bywdaylist", value)); } }); // ( ";" "BYMONTHDAY" "=" bymodaylist ) / contentRules.put("BYMONTHDAY", new ContentRule() { public void apply(IcalSchema schema, String value, IcalObject target) throws ParseException { ((RRule) target).setByMonthDay( (int[]) schema.applyXformSchema("bymodaylist", value)); } }); // ( ";" "BYYEARDAY" "=" byyrdaylist ) / contentRules.put("BYYEARDAY", new ContentRule() { public void apply(IcalSchema schema, String value, IcalObject target) throws ParseException { ((RRule) target).setByYearDay( (int[]) schema.applyXformSchema("byyrdaylist", value)); } }); // ( ";" "BYWEEKNO" "=" bywknolist ) / contentRules.put("BYWEEKNO", new ContentRule() { public void apply(IcalSchema schema, String value, IcalObject target) throws ParseException { ((RRule) target).setByWeekNo( (int[]) schema.applyXformSchema("bywknolist", value)); } }); // ( ";" "BYMONTH" "=" bymolist ) / contentRules.put("BYMONTH", new ContentRule() { public void apply(IcalSchema schema, String value, IcalObject target) throws ParseException { ((RRule) target).setByMonth( (int[]) schema.applyXformSchema("bymolist", value)); } }); // ( ";" "BYSETPOS" "=" bysplist ) / contentRules.put("BYSETPOS", new ContentRule() { public void apply(IcalSchema schema, String value, IcalObject target) throws ParseException { ((RRule) target).setBySetPos( (int[]) schema.applyXformSchema("bysplist", value)); } }); // ( ";" "WKST" "=" weekday ) / contentRules.put("WKST", new ContentRule() { public void apply(IcalSchema schema, String value, IcalObject target) throws ParseException { ((RRule) target).setWkSt( (Weekday) schema.applyXformSchema("weekday", value)); } }); // freq = "SECONDLY" / "MINUTELY" / "HOURLY" / "DAILY" // / "WEEKLY" / "MONTHLY" / "YEARLY" xformRules.put("freq", new XformRule() { public Frequency apply(IcalSchema schema, String value) throws ParseException { return Frequency.valueOf(value); } }); // enddate = date // enddate =/ date-time ;An UTC value xformRules.put("enddate", new XformRule() { public DateValue apply(IcalSchema schema, String value) throws ParseException { return IcalParseUtil.parseDateValue(value.toUpperCase()); } }); // byseclist = seconds / ( seconds *("," seconds) ) // seconds = 1DIGIT / 2DIGIT ;0 to 59 xformRules.put("byseclist", new XformRule() { public int[] apply(IcalSchema schema, String value) throws ParseException { return parseUnsignedIntList(value, 0, 59, schema); } }); // byminlist = minutes / ( minutes *("," minutes) ) // minutes = 1DIGIT / 2DIGIT ;0 to 59 xformRules.put("byminlist", new XformRule() { public int[] apply(IcalSchema schema, String value) throws ParseException { return parseUnsignedIntList(value, 0, 59, schema); } }); // byhrlist = hour / ( hour *("," hour) ) // hour = 1DIGIT / 2DIGIT ;0 to 23 xformRules.put("byhrlist", new XformRule() { public int[] apply(IcalSchema schema, String value) throws ParseException { return parseUnsignedIntList(value, 0, 23, schema); } }); // plus = "+" // minus = "-" // bywdaylist = weekdaynum / ( weekdaynum *("," weekdaynum) ) // weekdaynum = [([plus] ordwk / minus ordwk)] weekday // ordwk = 1DIGIT / 2DIGIT ;1 to 53 // weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA" // ;Corresponding to SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, // ;FRIDAY, SATURDAY and SUNDAY days of the week. xformRules.put("bywdaylist", new XformRule() { public List<WeekdayNum> apply(IcalSchema schema, String value) throws ParseException { String[] parts = COMMA.split(value); List<WeekdayNum> weekdays = new ArrayList<WeekdayNum>(parts.length); for (String p : parts) { Matcher m = NUM_DAY.matcher(p); if (!m.matches()) { schema.badPart(p, null); } Weekday wday = Weekday.valueOf(m.group(2).toUpperCase()); int n; String numText = m.group(1); if (null == numText || "".equals(numText)) { n = 0; } else { n = Integer.parseInt(numText); int absn = n < 0 ? -n : n; if (!(1 <= absn && 53 >= absn)) { schema.badPart(p, null); } } weekdays.add(new WeekdayNum(n, wday)); } return weekdays; } }); xformRules.put("weekday", new XformRule() { public Weekday apply(IcalSchema schema, String value) throws ParseException { return Weekday.valueOf(value.toUpperCase()); } }); // bymodaylist = monthdaynum / ( monthdaynum *("," monthdaynum) ) // monthdaynum = ([plus] ordmoday) / (minus ordmoday) // ordmoday = 1DIGIT / 2DIGIT ;1 to 31 xformRules.put("bymodaylist", new XformRule() { public int[] apply(IcalSchema schema, String value) throws ParseException { return parseIntList(value, 1, 31, schema); } }); // byyrdaylist = yeardaynum / ( yeardaynum *("," yeardaynum) ) // yeardaynum = ([plus] ordyrday) / (minus ordyrday) // ordyrday = 1DIGIT / 2DIGIT / 3DIGIT ;1 to 366 xformRules.put("byyrdaylist", new XformRule() { public int[] apply(IcalSchema schema, String value) throws ParseException { return parseIntList(value, 1, 366, schema); } }); // bywknolist = weeknum / ( weeknum *("," weeknum) ) // weeknum = ([plus] ordwk) / (minus ordwk) xformRules.put("bywknolist", new XformRule() { public int[] apply(IcalSchema schema, String value) throws ParseException { return parseIntList(value, 1, 53, schema); } }); // bymolist = monthnum / ( monthnum *("," monthnum) ) // monthnum = 1DIGIT / 2DIGIT ;1 to 12 xformRules.put("bymolist", new XformRule() { public int[] apply(IcalSchema schema, String value) throws ParseException { return parseIntList(value, 1, 12, schema); } }); // bysplist = setposday / ( setposday *("," setposday) ) // setposday = yeardaynum xformRules.put("bysplist", new XformRule() { public int[] apply(IcalSchema schema, String value) throws ParseException { return parseIntList(value, 1, 366, schema); } }); // rdate = "RDATE" rdtparam ":" rdtval *("," rdtval) CRLF objectRules.put("RDATE", new ObjectRule() { public void apply( IcalSchema schema, Map<String, String> params, String content, IcalObject target) throws ParseException { schema.applyParamsSchema("rdtparam", params, target); schema.applyContentSchema("rdtval", content, target); } }); // exdate = "EXDATE" exdtparam ":" exdtval *("," exdtval) CRLF // exdtparam = rdtparam // exdtval = rdtval objectRules.put("EXDATE", new ObjectRule() { public void apply( IcalSchema schema, Map<String, String> params, String content, IcalObject target) throws ParseException { schema.applyParamsSchema("rdtparam", params, target); schema.applyContentSchema("rdtval", content, target); } }); // rdtparam = *( // ; the following are optional, // ; but MUST NOT occur more than once // (";" "VALUE" "=" ("DATE-TIME" / "DATE" / "PERIOD")) / // (";" tzidparam) / // ; the following is optional, // ; and MAY occur more than once // (";" xparam) // ) // tzidparam = "TZID" "=" [tzidprefix] paramtext CRLF // tzidprefix = "/" paramRules.put( "rdtparam", new ParamRule() { public void apply( IcalSchema schema, String name, String value, IcalObject out) throws ParseException { if ("value".equalsIgnoreCase(name)) { if ("date-time".equalsIgnoreCase(value) || "date".equalsIgnoreCase(value) || "period".equalsIgnoreCase(value)) { ((RDateList) out).setValueType(IcalValueType.fromIcal(value)); } else { schema.badParam(name, value); } } else if ("tzid".equalsIgnoreCase(name)) { if (value.startsWith("/")) { // is globally defined name. We treat all as globally defined. value = value.substring(1).trim(); } // TODO(msamuel): proper timezone lookup, and warn on failure TimeZone tz = TimeUtils.timeZoneForName( value.replaceAll(" ", "_")); if (null == tz) { schema.badParam(name, value); } ((RDateList) out).setTzid(tz); } else { schema.badParam(name, value); } } }); paramRules.put("rrulparam", xparamsOnly); paramRules.put("exrparam", xparamsOnly); // rdtval = date-time / date / period ;Value MUST match value type contentRules.put("rdtval", new ContentRule() { public void apply(IcalSchema schema, String content, IcalObject target) throws ParseException { RDateList rdates = (RDateList) target; String[] parts = COMMA.split(content); DateValue[] datesUtc = new DateValue[parts.length]; for (int i = 0; i < parts.length; ++i) { String part = parts[i]; // TODO(msamuel): figure out what to do with periods. datesUtc[i] = IcalParseUtil.parseDateValue(part, rdates.getTzid()); } rdates.setDatesUtc(datesUtc); } }); PARAM_RULES = Collections.unmodifiableMap(paramRules); CONTENT_RULES = Collections.unmodifiableMap(contentRules); OBJECT_RULES = Collections.unmodifiableMap(objectRules); XFORM_RULES = Collections.unmodifiableMap(xformRules); } ///////////////////////////////// // Parser Helper functions and classes ///////////////////////////////// private static int[] parseIntList( String commaSeparatedString, int absmin, int absmax, IcalSchema schema) throws ParseException { String[] parts = COMMA.split(commaSeparatedString); int[] out = new int[parts.length]; for (int i = parts.length; --i >= 0;) { try { int n = Integer.parseInt(parts[i]); int absn = Math.abs(n); if (!(absmin <= absn && absmax >= absn)) { schema.badPart(commaSeparatedString, null); } out[i] = n; } catch (NumberFormatException ex) { schema.badPart(commaSeparatedString, ex.getMessage()); } } return out; } private static int[] parseUnsignedIntList( String commaSeparatedString, int min, int max, IcalSchema schema) throws ParseException { String[] parts = COMMA.split(commaSeparatedString); int[] out = new int[parts.length]; for (int i = parts.length; --i >= 0;) { try { int n = Integer.parseInt(parts[i]); if (!(min <= n && max >= n)) { schema.badPart(commaSeparatedString, null); } out[i] = n; } catch (NumberFormatException ex) { schema.badPart(commaSeparatedString, ex.getMessage()); } } return out; } }