Home | History | Annotate | Download | only in calendarcommon2
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.calendarcommon2;
     18 
     19 import android.util.Log;
     20 
     21 import java.util.LinkedHashMap;
     22 import java.util.LinkedList;
     23 import java.util.List;
     24 import java.util.Set;
     25 import java.util.ArrayList;
     26 
     27 /**
     28  * Parses RFC 2445 iCalendar objects.
     29  */
     30 public class ICalendar {
     31 
     32     private static final String TAG = "Sync";
     33 
     34     // TODO: keep track of VEVENT, VTODO, VJOURNAL, VFREEBUSY, VTIMEZONE, VALARM
     35     // components, by type field or by subclass?  subclass would allow us to
     36     // enforce grammars.
     37 
     38     /**
     39      * Exception thrown when an iCalendar object has invalid syntax.
     40      */
     41     public static class FormatException extends Exception {
     42         public FormatException() {
     43             super();
     44         }
     45 
     46         public FormatException(String msg) {
     47             super(msg);
     48         }
     49 
     50         public FormatException(String msg, Throwable cause) {
     51             super(msg, cause);
     52         }
     53     }
     54 
     55     /**
     56      * A component within an iCalendar (VEVENT, VTODO, VJOURNAL, VFEEBUSY,
     57      * VTIMEZONE, VALARM).
     58      */
     59     public static class Component {
     60 
     61         // components
     62         static final String BEGIN = "BEGIN";
     63         static final String END = "END";
     64         private static final String NEWLINE = "\n";
     65         public static final String VCALENDAR = "VCALENDAR";
     66         public static final String VEVENT = "VEVENT";
     67         public static final String VTODO = "VTODO";
     68         public static final String VJOURNAL = "VJOURNAL";
     69         public static final String VFREEBUSY = "VFREEBUSY";
     70         public static final String VTIMEZONE = "VTIMEZONE";
     71         public static final String VALARM = "VALARM";
     72 
     73         private final String mName;
     74         private final Component mParent; // see if we can get rid of this
     75         private LinkedList<Component> mChildren = null;
     76         private final LinkedHashMap<String, ArrayList<Property>> mPropsMap =
     77                 new LinkedHashMap<String, ArrayList<Property>>();
     78 
     79         /**
     80          * Creates a new component with the provided name.
     81          * @param name The name of the component.
     82          */
     83         public Component(String name, Component parent) {
     84             mName = name;
     85             mParent = parent;
     86         }
     87 
     88         /**
     89          * Returns the name of the component.
     90          * @return The name of the component.
     91          */
     92         public String getName() {
     93             return mName;
     94         }
     95 
     96         /**
     97          * Returns the parent of this component.
     98          * @return The parent of this component.
     99          */
    100         public Component getParent() {
    101             return mParent;
    102         }
    103 
    104         /**
    105          * Helper that lazily gets/creates the list of children.
    106          * @return The list of children.
    107          */
    108         protected LinkedList<Component> getOrCreateChildren() {
    109             if (mChildren == null) {
    110                 mChildren = new LinkedList<Component>();
    111             }
    112             return mChildren;
    113         }
    114 
    115         /**
    116          * Adds a child component to this component.
    117          * @param child The child component.
    118          */
    119         public void addChild(Component child) {
    120             getOrCreateChildren().add(child);
    121         }
    122 
    123         /**
    124          * Returns a list of the Component children of this component.  May be
    125          * null, if there are no children.
    126          *
    127          * @return A list of the children.
    128          */
    129         public List<Component> getComponents() {
    130             return mChildren;
    131         }
    132 
    133         /**
    134          * Adds a Property to this component.
    135          * @param prop
    136          */
    137         public void addProperty(Property prop) {
    138             String name= prop.getName();
    139             ArrayList<Property> props = mPropsMap.get(name);
    140             if (props == null) {
    141                 props = new ArrayList<Property>();
    142                 mPropsMap.put(name, props);
    143             }
    144             props.add(prop);
    145         }
    146 
    147         /**
    148          * Returns a set of the property names within this component.
    149          * @return A set of property names within this component.
    150          */
    151         public Set<String> getPropertyNames() {
    152             return mPropsMap.keySet();
    153         }
    154 
    155         /**
    156          * Returns a list of properties with the specified name.  Returns null
    157          * if there are no such properties.
    158          * @param name The name of the property that should be returned.
    159          * @return A list of properties with the requested name.
    160          */
    161         public List<Property> getProperties(String name) {
    162             return mPropsMap.get(name);
    163         }
    164 
    165         /**
    166          * Returns the first property with the specified name.  Returns null
    167          * if there is no such property.
    168          * @param name The name of the property that should be returned.
    169          * @return The first property with the specified name.
    170          */
    171         public Property getFirstProperty(String name) {
    172             List<Property> props = mPropsMap.get(name);
    173             if (props == null || props.size() == 0) {
    174                 return null;
    175             }
    176             return props.get(0);
    177         }
    178 
    179         @Override
    180         public String toString() {
    181             StringBuilder sb = new StringBuilder();
    182             toString(sb);
    183             sb.append(NEWLINE);
    184             return sb.toString();
    185         }
    186 
    187         /**
    188          * Helper method that appends this component to a StringBuilder.  The
    189          * caller is responsible for appending a newline at the end of the
    190          * component.
    191          */
    192         public void toString(StringBuilder sb) {
    193             sb.append(BEGIN);
    194             sb.append(":");
    195             sb.append(mName);
    196             sb.append(NEWLINE);
    197 
    198             // append the properties
    199             for (String propertyName : getPropertyNames()) {
    200                 for (Property property : getProperties(propertyName)) {
    201                     property.toString(sb);
    202                     sb.append(NEWLINE);
    203                 }
    204             }
    205 
    206             // append the sub-components
    207             if (mChildren != null) {
    208                 for (Component component : mChildren) {
    209                     component.toString(sb);
    210                     sb.append(NEWLINE);
    211                 }
    212             }
    213 
    214             sb.append(END);
    215             sb.append(":");
    216             sb.append(mName);
    217         }
    218     }
    219 
    220     /**
    221      * A property within an iCalendar component (e.g., DTSTART, DTEND, etc.,
    222      * within a VEVENT).
    223      */
    224     public static class Property {
    225         // properties
    226         // TODO: do we want to list these here?  the complete list is long.
    227         public static final String DTSTART = "DTSTART";
    228         public static final String DTEND = "DTEND";
    229         public static final String DURATION = "DURATION";
    230         public static final String RRULE = "RRULE";
    231         public static final String RDATE = "RDATE";
    232         public static final String EXRULE = "EXRULE";
    233         public static final String EXDATE = "EXDATE";
    234         // ... need to add more.
    235 
    236         private final String mName;
    237         private LinkedHashMap<String, ArrayList<Parameter>> mParamsMap =
    238                 new LinkedHashMap<String, ArrayList<Parameter>>();
    239         private String mValue; // TODO: make this final?
    240 
    241         /**
    242          * Creates a new property with the provided name.
    243          * @param name The name of the property.
    244          */
    245         public Property(String name) {
    246             mName = name;
    247         }
    248 
    249         /**
    250          * Creates a new property with the provided name and value.
    251          * @param name The name of the property.
    252          * @param value The value of the property.
    253          */
    254         public Property(String name, String value) {
    255             mName = name;
    256             mValue = value;
    257         }
    258 
    259         /**
    260          * Returns the name of the property.
    261          * @return The name of the property.
    262          */
    263         public String getName() {
    264             return mName;
    265         }
    266 
    267         /**
    268          * Returns the value of this property.
    269          * @return The value of this property.
    270          */
    271         public String getValue() {
    272             return mValue;
    273         }
    274 
    275         /**
    276          * Sets the value of this property.
    277          * @param value The desired value for this property.
    278          */
    279         public void setValue(String value) {
    280             mValue = value;
    281         }
    282 
    283         /**
    284          * Adds a {@link Parameter} to this property.
    285          * @param param The parameter that should be added.
    286          */
    287         public void addParameter(Parameter param) {
    288             ArrayList<Parameter> params = mParamsMap.get(param.name);
    289             if (params == null) {
    290                 params = new ArrayList<Parameter>();
    291                 mParamsMap.put(param.name, params);
    292             }
    293             params.add(param);
    294         }
    295 
    296         /**
    297          * Returns the set of parameter names for this property.
    298          * @return The set of parameter names for this property.
    299          */
    300         public Set<String> getParameterNames() {
    301             return mParamsMap.keySet();
    302         }
    303 
    304         /**
    305          * Returns the list of parameters with the specified name.  May return
    306          * null if there are no such parameters.
    307          * @param name The name of the parameters that should be returned.
    308          * @return The list of parameters with the specified name.
    309          */
    310         public List<Parameter> getParameters(String name) {
    311             return mParamsMap.get(name);
    312         }
    313 
    314         /**
    315          * Returns the first parameter with the specified name.  May return
    316          * nll if there is no such parameter.
    317          * @param name The name of the parameter that should be returned.
    318          * @return The first parameter with the specified name.
    319          */
    320         public Parameter getFirstParameter(String name) {
    321             ArrayList<Parameter> params = mParamsMap.get(name);
    322             if (params == null || params.size() == 0) {
    323                 return null;
    324             }
    325             return params.get(0);
    326         }
    327 
    328         @Override
    329         public String toString() {
    330             StringBuilder sb = new StringBuilder();
    331             toString(sb);
    332             return sb.toString();
    333         }
    334 
    335         /**
    336          * Helper method that appends this property to a StringBuilder.  The
    337          * caller is responsible for appending a newline after this property.
    338          */
    339         public void toString(StringBuilder sb) {
    340             sb.append(mName);
    341             Set<String> parameterNames = getParameterNames();
    342             for (String parameterName : parameterNames) {
    343                 for (Parameter param : getParameters(parameterName)) {
    344                     sb.append(";");
    345                     param.toString(sb);
    346                 }
    347             }
    348             sb.append(":");
    349             sb.append(mValue);
    350         }
    351     }
    352 
    353     /**
    354      * A parameter defined for an iCalendar property.
    355      */
    356     // TODO: make this a proper class rather than a struct?
    357     public static class Parameter {
    358         public String name;
    359         public String value;
    360 
    361         /**
    362          * Creates a new empty parameter.
    363          */
    364         public Parameter() {
    365         }
    366 
    367         /**
    368          * Creates a new parameter with the specified name and value.
    369          * @param name The name of the parameter.
    370          * @param value The value of the parameter.
    371          */
    372         public Parameter(String name, String value) {
    373             this.name = name;
    374             this.value = value;
    375         }
    376 
    377         @Override
    378         public String toString() {
    379             StringBuilder sb = new StringBuilder();
    380             toString(sb);
    381             return sb.toString();
    382         }
    383 
    384         /**
    385          * Helper method that appends this parameter to a StringBuilder.
    386          */
    387         public void toString(StringBuilder sb) {
    388             sb.append(name);
    389             sb.append("=");
    390             sb.append(value);
    391         }
    392     }
    393 
    394     private static final class ParserState {
    395         // public int lineNumber = 0;
    396         public String line; // TODO: just point to original text
    397         public int index;
    398     }
    399 
    400     // use factory method
    401     private ICalendar() {
    402     }
    403 
    404     // TODO: get rid of this -- handle all of the parsing in one pass through
    405     // the text.
    406     private static String normalizeText(String text) {
    407         // it's supposed to be \r\n, but not everyone does that
    408         text = text.replaceAll("\r\n", "\n");
    409         text = text.replaceAll("\r", "\n");
    410 
    411         // we deal with line folding, by replacing all "\n " strings
    412         // with nothing.  The RFC specifies "\r\n " to be folded, but
    413         // we handle "\n " and "\r " too because we can get those.
    414         text = text.replaceAll("\n ", "");
    415 
    416         return text;
    417     }
    418 
    419     /**
    420      * Parses text into an iCalendar component.  Parses into the provided
    421      * component, if not null, or parses into a new component.  In the latter
    422      * case, expects a BEGIN as the first line.  Returns the provided or newly
    423      * created top-level component.
    424      */
    425     // TODO: use an index into the text, so we can make this a recursive
    426     // function?
    427     private static Component parseComponentImpl(Component component,
    428                                                 String text)
    429             throws FormatException {
    430         Component current = component;
    431         ParserState state = new ParserState();
    432         state.index = 0;
    433 
    434         // split into lines
    435         String[] lines = text.split("\n");
    436 
    437         // each line is of the format:
    438         // name *(";" param) ":" value
    439         for (String line : lines) {
    440             try {
    441                 current = parseLine(line, state, current);
    442                 // if the provided component was null, we will return the root
    443                 // NOTE: in this case, if the first line is not a BEGIN, a
    444                 // FormatException will get thrown.
    445                 if (component == null) {
    446                     component = current;
    447                 }
    448             } catch (FormatException fe) {
    449                 if (false) {
    450                     Log.v(TAG, "Cannot parse " + line, fe);
    451                 }
    452                 // for now, we ignore the parse error.  Google Calendar seems
    453                 // to be emitting some misformatted iCalendar objects.
    454             }
    455             continue;
    456         }
    457         return component;
    458     }
    459 
    460     /**
    461      * Parses a line into the provided component.  Creates a new component if
    462      * the line is a BEGIN, adding the newly created component to the provided
    463      * parent.  Returns whatever component is the current one (to which new
    464      * properties will be added) in the parse.
    465      */
    466     private static Component parseLine(String line, ParserState state,
    467                                        Component component)
    468             throws FormatException {
    469         state.line = line;
    470         int len = state.line.length();
    471 
    472         // grab the name
    473         char c = 0;
    474         for (state.index = 0; state.index < len; ++state.index) {
    475             c = line.charAt(state.index);
    476             if (c == ';' || c == ':') {
    477                 break;
    478             }
    479         }
    480         String name = line.substring(0, state.index);
    481 
    482         if (component == null) {
    483             if (!Component.BEGIN.equals(name)) {
    484                 throw new FormatException("Expected BEGIN");
    485             }
    486         }
    487 
    488         Property property;
    489         if (Component.BEGIN.equals(name)) {
    490             // start a new component
    491             String componentName = extractValue(state);
    492             Component child = new Component(componentName, component);
    493             if (component != null) {
    494                 component.addChild(child);
    495             }
    496             return child;
    497         } else if (Component.END.equals(name)) {
    498             // finish the current component
    499             String componentName = extractValue(state);
    500             if (component == null ||
    501                     !componentName.equals(component.getName())) {
    502                 throw new FormatException("Unexpected END " + componentName);
    503             }
    504             return component.getParent();
    505         } else {
    506             property = new Property(name);
    507         }
    508 
    509         if (c == ';') {
    510             Parameter parameter = null;
    511             while ((parameter = extractParameter(state)) != null) {
    512                 property.addParameter(parameter);
    513             }
    514         }
    515         String value = extractValue(state);
    516         property.setValue(value);
    517         component.addProperty(property);
    518         return component;
    519     }
    520 
    521     /**
    522      * Extracts the value ":..." on the current line.  The first character must
    523      * be a ':'.
    524      */
    525     private static String extractValue(ParserState state)
    526             throws FormatException {
    527         String line = state.line;
    528         if (state.index >= line.length() || line.charAt(state.index) != ':') {
    529             throw new FormatException("Expected ':' before end of line in "
    530                     + line);
    531         }
    532         String value = line.substring(state.index + 1);
    533         state.index = line.length() - 1;
    534         return value;
    535     }
    536 
    537     /**
    538      * Extracts the next parameter from the line, if any.  If there are no more
    539      * parameters, returns null.
    540      */
    541     private static Parameter extractParameter(ParserState state)
    542             throws FormatException {
    543         String text = state.line;
    544         int len = text.length();
    545         Parameter parameter = null;
    546         int startIndex = -1;
    547         int equalIndex = -1;
    548         while (state.index < len) {
    549             char c = text.charAt(state.index);
    550             if (c == ':') {
    551                 if (parameter != null) {
    552                     if (equalIndex == -1) {
    553                         throw new FormatException("Expected '=' within "
    554                                 + "parameter in " + text);
    555                     }
    556                     parameter.value = text.substring(equalIndex + 1,
    557                                                      state.index);
    558                 }
    559                 return parameter; // may be null
    560             } else if (c == ';') {
    561                 if (parameter != null) {
    562                     if (equalIndex == -1) {
    563                         throw new FormatException("Expected '=' within "
    564                                 + "parameter in " + text);
    565                     }
    566                     parameter.value = text.substring(equalIndex + 1,
    567                                                      state.index);
    568                     return parameter;
    569                 } else {
    570                     parameter = new Parameter();
    571                     startIndex = state.index;
    572                 }
    573             } else if (c == '=') {
    574                 equalIndex = state.index;
    575                 if ((parameter == null) || (startIndex == -1)) {
    576                     throw new FormatException("Expected ';' before '=' in "
    577                             + text);
    578                 }
    579                 parameter.name = text.substring(startIndex + 1, equalIndex);
    580             } else if (c == '"') {
    581                 if (parameter == null) {
    582                     throw new FormatException("Expected parameter before '\"' in " + text);
    583                 }
    584                 if (equalIndex == -1) {
    585                     throw new FormatException("Expected '=' within parameter in " + text);
    586                 }
    587                 if (state.index > equalIndex + 1) {
    588                     throw new FormatException("Parameter value cannot contain a '\"' in " + text);
    589                 }
    590                 final int endQuote = text.indexOf('"', state.index + 1);
    591                 if (endQuote < 0) {
    592                     throw new FormatException("Expected closing '\"' in " + text);
    593                 }
    594                 parameter.value = text.substring(state.index + 1, endQuote);
    595                 state.index = endQuote + 1;
    596                 return parameter;
    597             }
    598             ++state.index;
    599         }
    600         throw new FormatException("Expected ':' before end of line in " + text);
    601     }
    602 
    603     /**
    604      * Parses the provided text into an iCalendar object.  The top-level
    605      * component must be of type VCALENDAR.
    606      * @param text The text to be parsed.
    607      * @return The top-level VCALENDAR component.
    608      * @throws FormatException Thrown if the text could not be parsed into an
    609      * iCalendar VCALENDAR object.
    610      */
    611     public static Component parseCalendar(String text) throws FormatException {
    612         Component calendar = parseComponent(null, text);
    613         if (calendar == null || !Component.VCALENDAR.equals(calendar.getName())) {
    614             throw new FormatException("Expected " + Component.VCALENDAR);
    615         }
    616         return calendar;
    617     }
    618 
    619     /**
    620      * Parses the provided text into an iCalendar event.  The top-level
    621      * component must be of type VEVENT.
    622      * @param text The text to be parsed.
    623      * @return The top-level VEVENT component.
    624      * @throws FormatException Thrown if the text could not be parsed into an
    625      * iCalendar VEVENT.
    626      */
    627     public static Component parseEvent(String text) throws FormatException {
    628         Component event = parseComponent(null, text);
    629         if (event == null || !Component.VEVENT.equals(event.getName())) {
    630             throw new FormatException("Expected " + Component.VEVENT);
    631         }
    632         return event;
    633     }
    634 
    635     /**
    636      * Parses the provided text into an iCalendar component.
    637      * @param text The text to be parsed.
    638      * @return The top-level component.
    639      * @throws FormatException Thrown if the text could not be parsed into an
    640      * iCalendar component.
    641      */
    642     public static Component parseComponent(String text) throws FormatException {
    643         return parseComponent(null, text);
    644     }
    645 
    646     /**
    647      * Parses the provided text, adding to the provided component.
    648      * @param component The component to which the parsed iCalendar data should
    649      * be added.
    650      * @param text The text to be parsed.
    651      * @return The top-level component.
    652      * @throws FormatException Thrown if the text could not be parsed as an
    653      * iCalendar object.
    654      */
    655     public static Component parseComponent(Component component, String text)
    656         throws FormatException {
    657         text = normalizeText(text);
    658         return parseComponentImpl(component, text);
    659     }
    660 }
    661