All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.ical.values.RRuleSchema Maven / Gradle / Ivy

// 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 [email protected] (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 PARAM_RULES;

  private static final Map CONTENT_RULES;

  private static final Map OBJECT_RULES;

  private static final Map XFORM_RULES;

  static {
    Map paramRules = new HashMap();
    Map contentRules = new HashMap();
    Map objectRules = new HashMap();
    Map xformRules = new HashMap();

    // rrule      = "RRULE" rrulparam ":" recur CRLF
    // exrule     = "EXRULE" exrparam ":" recur CRLF
    objectRules.put("RRULE", new ObjectRule() {
        public void apply(
            IcalSchema schema, Map 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 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 partMap = new HashMap();
          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 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 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) 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 apply(IcalSchema schema, String value)
            throws ParseException {
          String[] parts = COMMA.split(value);
          List weekdays = new ArrayList(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 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 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;
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy