summaryrefslogtreecommitdiffstats
path: root/core/java/android/pim/ICalendar.java
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/pim/ICalendar.java')
-rw-r--r--core/java/android/pim/ICalendar.java643
1 files changed, 643 insertions, 0 deletions
diff --git a/core/java/android/pim/ICalendar.java b/core/java/android/pim/ICalendar.java
new file mode 100644
index 0000000..4a5d7e4
--- /dev/null
+++ b/core/java/android/pim/ICalendar.java
@@ -0,0 +1,643 @@
+/*
+ * 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 android.pim;
+
+import android.util.Log;
+import android.util.Config;
+
+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
+ private static final String BEGIN = "BEGIN";
+ private 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) {
+ // first we deal with line folding, by replacing all "\r\n " strings
+ // with nothing
+ text = text.replaceAll("\r\n ", "");
+
+ // it's supposed to be \r\n, but not everyone does that
+ text = text.replaceAll("\r\n", "\n");
+ text = text.replaceAll("\r", "\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 (Config.LOGV) {
+ 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;
+ char c = line.charAt(state.index);
+ if (c != ':') {
+ 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);
+ }
+ ++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);
+ }
+}