content: /*
   * 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);
      }
  }
  