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