| /* |
| * Copyright (C) 2007 The Android Open Source Project |
| * |
| * 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.android.calendarcommon2; |
| |
| import android.util.Log; |
| |
| import java.util.LinkedHashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.ArrayList; |
| |
| /** |
| * Parses RFC 2445 iCalendar objects. |
| */ |
| public class ICalendar { |
| |
| private static final String TAG = "Sync"; |
| |
| // TODO: keep track of VEVENT, VTODO, VJOURNAL, VFREEBUSY, VTIMEZONE, VALARM |
| // components, by type field or by subclass? subclass would allow us to |
| // enforce grammars. |
| |
| /** |
| * Exception thrown when an iCalendar object has invalid syntax. |
| */ |
| public static class FormatException extends Exception { |
| public FormatException() { |
| super(); |
| } |
| |
| public FormatException(String msg) { |
| super(msg); |
| } |
| |
| public FormatException(String msg, Throwable cause) { |
| super(msg, cause); |
| } |
| } |
| |
| /** |
| * A component within an iCalendar (VEVENT, VTODO, VJOURNAL, VFEEBUSY, |
| * VTIMEZONE, VALARM). |
| */ |
| public static class Component { |
| |
| // components |
| static final String BEGIN = "BEGIN"; |
| static final String END = "END"; |
| private static final String NEWLINE = "\n"; |
| public static final String VCALENDAR = "VCALENDAR"; |
| public static final String VEVENT = "VEVENT"; |
| public static final String VTODO = "VTODO"; |
| public static final String VJOURNAL = "VJOURNAL"; |
| public static final String VFREEBUSY = "VFREEBUSY"; |
| public static final String VTIMEZONE = "VTIMEZONE"; |
| public static final String VALARM = "VALARM"; |
| |
| private final String mName; |
| private final Component mParent; // see if we can get rid of this |
| private LinkedList<Component> mChildren = null; |
| private final LinkedHashMap<String, ArrayList<Property>> mPropsMap = |
| new LinkedHashMap<String, ArrayList<Property>>(); |
| |
| /** |
| * Creates a new component with the provided name. |
| * @param name The name of the component. |
| */ |
| public Component(String name, Component parent) { |
| mName = name; |
| mParent = parent; |
| } |
| |
| /** |
| * Returns the name of the component. |
| * @return The name of the component. |
| */ |
| public String getName() { |
| return mName; |
| } |
| |
| /** |
| * Returns the parent of this component. |
| * @return The parent of this component. |
| */ |
| public Component getParent() { |
| return mParent; |
| } |
| |
| /** |
| * Helper that lazily gets/creates the list of children. |
| * @return The list of children. |
| */ |
| protected LinkedList<Component> getOrCreateChildren() { |
| if (mChildren == null) { |
| mChildren = new LinkedList<Component>(); |
| } |
| return mChildren; |
| } |
| |
| /** |
| * Adds a child component to this component. |
| * @param child The child component. |
| */ |
| public void addChild(Component child) { |
| getOrCreateChildren().add(child); |
| } |
| |
| /** |
| * Returns a list of the Component children of this component. May be |
| * null, if there are no children. |
| * |
| * @return A list of the children. |
| */ |
| public List<Component> getComponents() { |
| return mChildren; |
| } |
| |
| /** |
| * Adds a Property to this component. |
| * @param prop |
| */ |
| public void addProperty(Property prop) { |
| String name= prop.getName(); |
| ArrayList<Property> props = mPropsMap.get(name); |
| if (props == null) { |
| props = new ArrayList<Property>(); |
| mPropsMap.put(name, props); |
| } |
| props.add(prop); |
| } |
| |
| /** |
| * Returns a set of the property names within this component. |
| * @return A set of property names within this component. |
| */ |
| public Set<String> getPropertyNames() { |
| return mPropsMap.keySet(); |
| } |
| |
| /** |
| * Returns a list of properties with the specified name. Returns null |
| * if there are no such properties. |
| * @param name The name of the property that should be returned. |
| * @return A list of properties with the requested name. |
| */ |
| public List<Property> getProperties(String name) { |
| return mPropsMap.get(name); |
| } |
| |
| /** |
| * Returns the first property with the specified name. Returns null |
| * if there is no such property. |
| * @param name The name of the property that should be returned. |
| * @return The first property with the specified name. |
| */ |
| public Property getFirstProperty(String name) { |
| List<Property> props = mPropsMap.get(name); |
| if (props == null || props.size() == 0) { |
| return null; |
| } |
| return props.get(0); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| toString(sb); |
| sb.append(NEWLINE); |
| return sb.toString(); |
| } |
| |
| /** |
| * Helper method that appends this component to a StringBuilder. The |
| * caller is responsible for appending a newline at the end of the |
| * component. |
| */ |
| public void toString(StringBuilder sb) { |
| sb.append(BEGIN); |
| sb.append(":"); |
| sb.append(mName); |
| sb.append(NEWLINE); |
| |
| // append the properties |
| for (String propertyName : getPropertyNames()) { |
| for (Property property : getProperties(propertyName)) { |
| property.toString(sb); |
| sb.append(NEWLINE); |
| } |
| } |
| |
| // append the sub-components |
| if (mChildren != null) { |
| for (Component component : mChildren) { |
| component.toString(sb); |
| sb.append(NEWLINE); |
| } |
| } |
| |
| sb.append(END); |
| sb.append(":"); |
| sb.append(mName); |
| } |
| } |
| |
| /** |
| * A property within an iCalendar component (e.g., DTSTART, DTEND, etc., |
| * within a VEVENT). |
| */ |
| public static class Property { |
| // properties |
| // TODO: do we want to list these here? the complete list is long. |
| public static final String DTSTART = "DTSTART"; |
| public static final String DTEND = "DTEND"; |
| public static final String DURATION = "DURATION"; |
| public static final String RRULE = "RRULE"; |
| public static final String RDATE = "RDATE"; |
| public static final String EXRULE = "EXRULE"; |
| public static final String EXDATE = "EXDATE"; |
| // ... need to add more. |
| |
| private final String mName; |
| private LinkedHashMap<String, ArrayList<Parameter>> mParamsMap = |
| new LinkedHashMap<String, ArrayList<Parameter>>(); |
| private String mValue; // TODO: make this final? |
| |
| /** |
| * Creates a new property with the provided name. |
| * @param name The name of the property. |
| */ |
| public Property(String name) { |
| mName = name; |
| } |
| |
| /** |
| * Creates a new property with the provided name and value. |
| * @param name The name of the property. |
| * @param value The value of the property. |
| */ |
| public Property(String name, String value) { |
| mName = name; |
| mValue = value; |
| } |
| |
| /** |
| * Returns the name of the property. |
| * @return The name of the property. |
| */ |
| public String getName() { |
| return mName; |
| } |
| |
| /** |
| * Returns the value of this property. |
| * @return The value of this property. |
| */ |
| public String getValue() { |
| return mValue; |
| } |
| |
| /** |
| * Sets the value of this property. |
| * @param value The desired value for this property. |
| */ |
| public void setValue(String value) { |
| mValue = value; |
| } |
| |
| /** |
| * Adds a {@link Parameter} to this property. |
| * @param param The parameter that should be added. |
| */ |
| public void addParameter(Parameter param) { |
| ArrayList<Parameter> params = mParamsMap.get(param.name); |
| if (params == null) { |
| params = new ArrayList<Parameter>(); |
| mParamsMap.put(param.name, params); |
| } |
| params.add(param); |
| } |
| |
| /** |
| * Returns the set of parameter names for this property. |
| * @return The set of parameter names for this property. |
| */ |
| public Set<String> getParameterNames() { |
| return mParamsMap.keySet(); |
| } |
| |
| /** |
| * Returns the list of parameters with the specified name. May return |
| * null if there are no such parameters. |
| * @param name The name of the parameters that should be returned. |
| * @return The list of parameters with the specified name. |
| */ |
| public List<Parameter> getParameters(String name) { |
| return mParamsMap.get(name); |
| } |
| |
| /** |
| * Returns the first parameter with the specified name. May return |
| * nll if there is no such parameter. |
| * @param name The name of the parameter that should be returned. |
| * @return The first parameter with the specified name. |
| */ |
| public Parameter getFirstParameter(String name) { |
| ArrayList<Parameter> params = mParamsMap.get(name); |
| if (params == null || params.size() == 0) { |
| return null; |
| } |
| return params.get(0); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| toString(sb); |
| return sb.toString(); |
| } |
| |
| /** |
| * Helper method that appends this property to a StringBuilder. The |
| * caller is responsible for appending a newline after this property. |
| */ |
| public void toString(StringBuilder sb) { |
| sb.append(mName); |
| Set<String> parameterNames = getParameterNames(); |
| for (String parameterName : parameterNames) { |
| for (Parameter param : getParameters(parameterName)) { |
| sb.append(";"); |
| param.toString(sb); |
| } |
| } |
| sb.append(":"); |
| sb.append(mValue); |
| } |
| } |
| |
| /** |
| * A parameter defined for an iCalendar property. |
| */ |
| // TODO: make this a proper class rather than a struct? |
| public static class Parameter { |
| public String name; |
| public String value; |
| |
| /** |
| * Creates a new empty parameter. |
| */ |
| public Parameter() { |
| } |
| |
| /** |
| * Creates a new parameter with the specified name and value. |
| * @param name The name of the parameter. |
| * @param value The value of the parameter. |
| */ |
| public Parameter(String name, String value) { |
| this.name = name; |
| this.value = value; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| toString(sb); |
| return sb.toString(); |
| } |
| |
| /** |
| * Helper method that appends this parameter to a StringBuilder. |
| */ |
| public void toString(StringBuilder sb) { |
| sb.append(name); |
| sb.append("="); |
| sb.append(value); |
| } |
| } |
| |
| private static final class ParserState { |
| // public int lineNumber = 0; |
| public String line; // TODO: just point to original text |
| public int index; |
| } |
| |
| // use factory method |
| private ICalendar() { |
| } |
| |
| // TODO: get rid of this -- handle all of the parsing in one pass through |
| // the text. |
| private static String normalizeText(String text) { |
| // it's supposed to be \r\n, but not everyone does that |
| text = text.replaceAll("\r\n", "\n"); |
| text = text.replaceAll("\r", "\n"); |
| |
| // we deal with line folding, by replacing all "\n " strings |
| // with nothing. The RFC specifies "\r\n " to be folded, but |
| // we handle "\n " and "\r " too because we can get those. |
| text = text.replaceAll("\n ", ""); |
| |
| return text; |
| } |
| |
| /** |
| * Parses text into an iCalendar component. Parses into the provided |
| * component, if not null, or parses into a new component. In the latter |
| * case, expects a BEGIN as the first line. Returns the provided or newly |
| * created top-level component. |
| */ |
| // TODO: use an index into the text, so we can make this a recursive |
| // function? |
| private static Component parseComponentImpl(Component component, |
| String text) |
| throws FormatException { |
| Component current = component; |
| ParserState state = new ParserState(); |
| state.index = 0; |
| |
| // split into lines |
| String[] lines = text.split("\n"); |
| |
| // each line is of the format: |
| // name *(";" param) ":" value |
| for (String line : lines) { |
| try { |
| current = parseLine(line, state, current); |
| // if the provided component was null, we will return the root |
| // NOTE: in this case, if the first line is not a BEGIN, a |
| // FormatException will get thrown. |
| if (component == null) { |
| component = current; |
| } |
| } catch (FormatException fe) { |
| if (false) { |
| Log.v(TAG, "Cannot parse " + line, fe); |
| } |
| // for now, we ignore the parse error. Google Calendar seems |
| // to be emitting some misformatted iCalendar objects. |
| } |
| continue; |
| } |
| return component; |
| } |
| |
| /** |
| * Parses a line into the provided component. Creates a new component if |
| * the line is a BEGIN, adding the newly created component to the provided |
| * parent. Returns whatever component is the current one (to which new |
| * properties will be added) in the parse. |
| */ |
| private static Component parseLine(String line, ParserState state, |
| Component component) |
| throws FormatException { |
| state.line = line; |
| int len = state.line.length(); |
| |
| // grab the name |
| char c = 0; |
| for (state.index = 0; state.index < len; ++state.index) { |
| c = line.charAt(state.index); |
| if (c == ';' || c == ':') { |
| break; |
| } |
| } |
| String name = line.substring(0, state.index); |
| |
| if (component == null) { |
| if (!Component.BEGIN.equals(name)) { |
| throw new FormatException("Expected BEGIN"); |
| } |
| } |
| |
| Property property; |
| if (Component.BEGIN.equals(name)) { |
| // start a new component |
| String componentName = extractValue(state); |
| Component child = new Component(componentName, component); |
| if (component != null) { |
| component.addChild(child); |
| } |
| return child; |
| } else if (Component.END.equals(name)) { |
| // finish the current component |
| String componentName = extractValue(state); |
| if (component == null || |
| !componentName.equals(component.getName())) { |
| throw new FormatException("Unexpected END " + componentName); |
| } |
| return component.getParent(); |
| } else { |
| property = new Property(name); |
| } |
| |
| if (c == ';') { |
| Parameter parameter = null; |
| while ((parameter = extractParameter(state)) != null) { |
| property.addParameter(parameter); |
| } |
| } |
| String value = extractValue(state); |
| property.setValue(value); |
| component.addProperty(property); |
| return component; |
| } |
| |
| /** |
| * Extracts the value ":..." on the current line. The first character must |
| * be a ':'. |
| */ |
| private static String extractValue(ParserState state) |
| throws FormatException { |
| String line = state.line; |
| if (state.index >= line.length() || line.charAt(state.index) != ':') { |
| throw new FormatException("Expected ':' before end of line in " |
| + line); |
| } |
| String value = line.substring(state.index + 1); |
| state.index = line.length() - 1; |
| return value; |
| } |
| |
| /** |
| * Extracts the next parameter from the line, if any. If there are no more |
| * parameters, returns null. |
| */ |
| private static Parameter extractParameter(ParserState state) |
| throws FormatException { |
| String text = state.line; |
| int len = text.length(); |
| Parameter parameter = null; |
| int startIndex = -1; |
| int equalIndex = -1; |
| while (state.index < len) { |
| char c = text.charAt(state.index); |
| if (c == ':') { |
| if (parameter != null) { |
| if (equalIndex == -1) { |
| throw new FormatException("Expected '=' within " |
| + "parameter in " + text); |
| } |
| parameter.value = text.substring(equalIndex + 1, |
| state.index); |
| } |
| return parameter; // may be null |
| } else if (c == ';') { |
| if (parameter != null) { |
| if (equalIndex == -1) { |
| throw new FormatException("Expected '=' within " |
| + "parameter in " + text); |
| } |
| parameter.value = text.substring(equalIndex + 1, |
| state.index); |
| return parameter; |
| } else { |
| parameter = new Parameter(); |
| startIndex = state.index; |
| } |
| } else if (c == '=') { |
| equalIndex = state.index; |
| if ((parameter == null) || (startIndex == -1)) { |
| throw new FormatException("Expected ';' before '=' in " |
| + text); |
| } |
| parameter.name = text.substring(startIndex + 1, equalIndex); |
| } else if (c == '"') { |
| if (parameter == null) { |
| throw new FormatException("Expected parameter before '\"' in " + text); |
| } |
| if (equalIndex == -1) { |
| throw new FormatException("Expected '=' within parameter in " + text); |
| } |
| if (state.index > equalIndex + 1) { |
| throw new FormatException("Parameter value cannot contain a '\"' in " + text); |
| } |
| final int endQuote = text.indexOf('"', state.index + 1); |
| if (endQuote < 0) { |
| throw new FormatException("Expected closing '\"' in " + text); |
| } |
| parameter.value = text.substring(state.index + 1, endQuote); |
| state.index = endQuote + 1; |
| return parameter; |
| } |
| ++state.index; |
| } |
| throw new FormatException("Expected ':' before end of line in " + text); |
| } |
| |
| /** |
| * Parses the provided text into an iCalendar object. The top-level |
| * component must be of type VCALENDAR. |
| * @param text The text to be parsed. |
| * @return The top-level VCALENDAR component. |
| * @throws FormatException Thrown if the text could not be parsed into an |
| * iCalendar VCALENDAR object. |
| */ |
| public static Component parseCalendar(String text) throws FormatException { |
| Component calendar = parseComponent(null, text); |
| if (calendar == null || !Component.VCALENDAR.equals(calendar.getName())) { |
| throw new FormatException("Expected " + Component.VCALENDAR); |
| } |
| return calendar; |
| } |
| |
| /** |
| * Parses the provided text into an iCalendar event. The top-level |
| * component must be of type VEVENT. |
| * @param text The text to be parsed. |
| * @return The top-level VEVENT component. |
| * @throws FormatException Thrown if the text could not be parsed into an |
| * iCalendar VEVENT. |
| */ |
| public static Component parseEvent(String text) throws FormatException { |
| Component event = parseComponent(null, text); |
| if (event == null || !Component.VEVENT.equals(event.getName())) { |
| throw new FormatException("Expected " + Component.VEVENT); |
| } |
| return event; |
| } |
| |
| /** |
| * Parses the provided text into an iCalendar component. |
| * @param text The text to be parsed. |
| * @return The top-level component. |
| * @throws FormatException Thrown if the text could not be parsed into an |
| * iCalendar component. |
| */ |
| public static Component parseComponent(String text) throws FormatException { |
| return parseComponent(null, text); |
| } |
| |
| /** |
| * Parses the provided text, adding to the provided component. |
| * @param component The component to which the parsed iCalendar data should |
| * be added. |
| * @param text The text to be parsed. |
| * @return The top-level component. |
| * @throws FormatException Thrown if the text could not be parsed as an |
| * iCalendar object. |
| */ |
| public static Component parseComponent(Component component, String text) |
| throws FormatException { |
| text = normalizeText(text); |
| return parseComponentImpl(component, text); |
| } |
| } |