10d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden/*
20d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * Copyright (C) 2007 The Android Open Source Project
30d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden *
40d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * Licensed under the Apache License, Version 2.0 (the "License");
50d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * you may not use this file except in compliance with the License.
60d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * You may obtain a copy of the License at
70d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden *
80d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden *      http://www.apache.org/licenses/LICENSE-2.0
90d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden *
100d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * Unless required by applicable law or agreed to in writing, software
110d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * distributed under the License is distributed on an "AS IS" BASIS,
120d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
130d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * See the License for the specific language governing permissions and
140d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * limitations under the License.
150d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden */
160d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1706b3293d5af3454a39681cfd659271551354b8a0Michael Chanpackage com.android.calendarcommon2;
180d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
190d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenimport android.util.Log;
200d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
210d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenimport java.util.LinkedHashMap;
220d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenimport java.util.LinkedList;
230d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenimport java.util.List;
240d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenimport java.util.Set;
250d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenimport java.util.ArrayList;
260d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
270d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden/**
280d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden * Parses RFC 2445 iCalendar objects.
290d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden */
300d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFaddenpublic class ICalendar {
310d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
320d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private static final String TAG = "Sync";
330d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
340d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    // TODO: keep track of VEVENT, VTODO, VJOURNAL, VFREEBUSY, VTIMEZONE, VALARM
350d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    // components, by type field or by subclass?  subclass would allow us to
360d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    // enforce grammars.
370d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
380d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
390d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * Exception thrown when an iCalendar object has invalid syntax.
400d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     */
410d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public static class FormatException extends Exception {
420d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public FormatException() {
430d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            super();
440d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
450d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
460d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public FormatException(String msg) {
470d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            super(msg);
480d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
490d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
500d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public FormatException(String msg, Throwable cause) {
510d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            super(msg, cause);
520d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
530d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
540d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
550d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
560d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * A component within an iCalendar (VEVENT, VTODO, VJOURNAL, VFEEBUSY,
570d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * VTIMEZONE, VALARM).
580d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     */
590d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public static class Component {
600d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
610d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // components
6274b1b5b02ac2022e01167d9cb5cbec02d3877fa7Conley Owens        static final String BEGIN = "BEGIN";
6374b1b5b02ac2022e01167d9cb5cbec02d3877fa7Conley Owens        static final String END = "END";
640d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        private static final String NEWLINE = "\n";
650d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public static final String VCALENDAR = "VCALENDAR";
660d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public static final String VEVENT = "VEVENT";
670d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public static final String VTODO = "VTODO";
680d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public static final String VJOURNAL = "VJOURNAL";
690d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public static final String VFREEBUSY = "VFREEBUSY";
700d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public static final String VTIMEZONE = "VTIMEZONE";
710d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public static final String VALARM = "VALARM";
720d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
730d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        private final String mName;
740d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        private final Component mParent; // see if we can get rid of this
750d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        private LinkedList<Component> mChildren = null;
760d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        private final LinkedHashMap<String, ArrayList<Property>> mPropsMap =
770d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                new LinkedHashMap<String, ArrayList<Property>>();
780d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
790d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
800d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Creates a new component with the provided name.
810d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @param name The name of the component.
820d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
830d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public Component(String name, Component parent) {
840d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            mName = name;
850d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            mParent = parent;
860d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
870d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
880d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
890d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Returns the name of the component.
900d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @return The name of the component.
910d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
920d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public String getName() {
930d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return mName;
940d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
950d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
960d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
970d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Returns the parent of this component.
980d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @return The parent of this component.
990d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
1000d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public Component getParent() {
1010d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return mParent;
1020d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
1030d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1040d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
1050d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Helper that lazily gets/creates the list of children.
1060d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @return The list of children.
1070d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
1080d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        protected LinkedList<Component> getOrCreateChildren() {
1090d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (mChildren == null) {
1100d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                mChildren = new LinkedList<Component>();
1110d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
1120d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return mChildren;
1130d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
1140d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1150d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
1160d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Adds a child component to this component.
1170d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @param child The child component.
1180d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
1190d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public void addChild(Component child) {
1200d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            getOrCreateChildren().add(child);
1210d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
1220d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1230d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
1240d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Returns a list of the Component children of this component.  May be
1250d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * null, if there are no children.
1260d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         *
1270d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @return A list of the children.
1280d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
1290d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public List<Component> getComponents() {
1300d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return mChildren;
1310d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
1320d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1330d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
1340d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Adds a Property to this component.
1350d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @param prop
1360d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
1370d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public void addProperty(Property prop) {
1380d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            String name= prop.getName();
1390d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            ArrayList<Property> props = mPropsMap.get(name);
1400d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (props == null) {
1410d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                props = new ArrayList<Property>();
1420d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                mPropsMap.put(name, props);
1430d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
1440d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            props.add(prop);
1450d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
1460d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1470d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
1480d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Returns a set of the property names within this component.
1490d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @return A set of property names within this component.
1500d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
1510d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public Set<String> getPropertyNames() {
1520d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return mPropsMap.keySet();
1530d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
1540d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1550d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
1560d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Returns a list of properties with the specified name.  Returns null
1570d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * if there are no such properties.
1580d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @param name The name of the property that should be returned.
1590d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @return A list of properties with the requested name.
1600d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
1610d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public List<Property> getProperties(String name) {
1620d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return mPropsMap.get(name);
1630d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
1640d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1650d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
1660d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Returns the first property with the specified name.  Returns null
1670d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * if there is no such property.
1680d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @param name The name of the property that should be returned.
1690d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @return The first property with the specified name.
1700d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
1710d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public Property getFirstProperty(String name) {
1720d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            List<Property> props = mPropsMap.get(name);
1730d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (props == null || props.size() == 0) {
1740d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                return null;
1750d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
1760d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return props.get(0);
1770d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
1780d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1790d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        @Override
1800d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public String toString() {
1810d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            StringBuilder sb = new StringBuilder();
1820d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            toString(sb);
1830d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            sb.append(NEWLINE);
1840d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return sb.toString();
1850d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
1860d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1870d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
1880d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Helper method that appends this component to a StringBuilder.  The
1890d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * caller is responsible for appending a newline at the end of the
1900d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * component.
1910d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
1920d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public void toString(StringBuilder sb) {
1930d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            sb.append(BEGIN);
1940d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            sb.append(":");
1950d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            sb.append(mName);
1960d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            sb.append(NEWLINE);
1970d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
1980d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            // append the properties
1990d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            for (String propertyName : getPropertyNames()) {
2000d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                for (Property property : getProperties(propertyName)) {
2010d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    property.toString(sb);
2020d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    sb.append(NEWLINE);
2030d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                }
2040d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
2050d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2060d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            // append the sub-components
2070d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (mChildren != null) {
2080d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                for (Component component : mChildren) {
2090d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    component.toString(sb);
2100d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    sb.append(NEWLINE);
2110d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                }
2120d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
2130d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2140d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            sb.append(END);
2150d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            sb.append(":");
2160d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            sb.append(mName);
2170d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
2180d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
2190d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2200d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
2210d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * A property within an iCalendar component (e.g., DTSTART, DTEND, etc.,
2220d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * within a VEVENT).
2230d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     */
2240d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public static class Property {
2250d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // properties
2260d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // TODO: do we want to list these here?  the complete list is long.
2270d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public static final String DTSTART = "DTSTART";
2280d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public static final String DTEND = "DTEND";
2290d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public static final String DURATION = "DURATION";
2300d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public static final String RRULE = "RRULE";
2310d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public static final String RDATE = "RDATE";
2320d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public static final String EXRULE = "EXRULE";
2330d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public static final String EXDATE = "EXDATE";
2340d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // ... need to add more.
2350d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2360d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        private final String mName;
2370d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        private LinkedHashMap<String, ArrayList<Parameter>> mParamsMap =
2380d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                new LinkedHashMap<String, ArrayList<Parameter>>();
2390d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        private String mValue; // TODO: make this final?
2400d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2410d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
2420d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Creates a new property with the provided name.
2430d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @param name The name of the property.
2440d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
2450d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public Property(String name) {
2460d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            mName = name;
2470d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
2480d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2490d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
2500d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Creates a new property with the provided name and value.
2510d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @param name The name of the property.
2520d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @param value The value of the property.
2530d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
2540d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public Property(String name, String value) {
2550d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            mName = name;
2560d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            mValue = value;
2570d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
2580d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2590d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
2600d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Returns the name of the property.
2610d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @return The name of the property.
2620d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
2630d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public String getName() {
2640d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return mName;
2650d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
2660d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2670d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
2680d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Returns the value of this property.
2690d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @return The value of this property.
2700d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
2710d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public String getValue() {
2720d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return mValue;
2730d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
2740d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2750d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
2760d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Sets the value of this property.
2770d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @param value The desired value for this property.
2780d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
2790d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public void setValue(String value) {
2800d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            mValue = value;
2810d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
2820d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2830d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
2840d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Adds a {@link Parameter} to this property.
2850d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @param param The parameter that should be added.
2860d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
2870d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public void addParameter(Parameter param) {
2880d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            ArrayList<Parameter> params = mParamsMap.get(param.name);
2890d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (params == null) {
2900d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                params = new ArrayList<Parameter>();
2910d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                mParamsMap.put(param.name, params);
2920d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
2930d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            params.add(param);
2940d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
2950d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
2960d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
2970d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Returns the set of parameter names for this property.
2980d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @return The set of parameter names for this property.
2990d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
3000d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public Set<String> getParameterNames() {
3010d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return mParamsMap.keySet();
3020d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
3030d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3040d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
3050d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Returns the list of parameters with the specified name.  May return
3060d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * null if there are no such parameters.
3070d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @param name The name of the parameters that should be returned.
3080d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @return The list of parameters with the specified name.
3090d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
3100d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public List<Parameter> getParameters(String name) {
3110d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return mParamsMap.get(name);
3120d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
3130d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3140d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
3150d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Returns the first parameter with the specified name.  May return
3160d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * nll if there is no such parameter.
3170d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @param name The name of the parameter that should be returned.
3180d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @return The first parameter with the specified name.
3190d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
3200d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public Parameter getFirstParameter(String name) {
3210d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            ArrayList<Parameter> params = mParamsMap.get(name);
3220d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (params == null || params.size() == 0) {
3230d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                return null;
3240d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
3250d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return params.get(0);
3260d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
3270d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3280d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        @Override
3290d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public String toString() {
3300d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            StringBuilder sb = new StringBuilder();
3310d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            toString(sb);
3320d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return sb.toString();
3330d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
3340d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3350d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
3360d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Helper method that appends this property to a StringBuilder.  The
3370d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * caller is responsible for appending a newline after this property.
3380d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
3390d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public void toString(StringBuilder sb) {
3400d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            sb.append(mName);
3410d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            Set<String> parameterNames = getParameterNames();
3420d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            for (String parameterName : parameterNames) {
3430d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                for (Parameter param : getParameters(parameterName)) {
3440d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    sb.append(";");
3450d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    param.toString(sb);
3460d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                }
3470d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
3480d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            sb.append(":");
3490d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            sb.append(mValue);
3500d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
3510d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
3520d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3530d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
3540d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * A parameter defined for an iCalendar property.
3550d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     */
3560d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    // TODO: make this a proper class rather than a struct?
3570d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public static class Parameter {
3580d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public String name;
3590d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public String value;
3600d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3610d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
3620d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Creates a new empty parameter.
3630d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
3640d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public Parameter() {
3650d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
3660d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3670d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
3680d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Creates a new parameter with the specified name and value.
3690d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @param name The name of the parameter.
3700d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * @param value The value of the parameter.
3710d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
3720d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public Parameter(String name, String value) {
3730d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            this.name = name;
3740d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            this.value = value;
3750d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
3760d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3770d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        @Override
3780d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public String toString() {
3790d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            StringBuilder sb = new StringBuilder();
3800d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            toString(sb);
3810d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return sb.toString();
3820d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
3830d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3840d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        /**
3850d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         * Helper method that appends this parameter to a StringBuilder.
3860d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden         */
3870d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public void toString(StringBuilder sb) {
3880d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            sb.append(name);
3890d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            sb.append("=");
3900d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            sb.append(value);
3910d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
3920d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
3930d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
3940d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private static final class ParserState {
3950d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // public int lineNumber = 0;
3960d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public String line; // TODO: just point to original text
3970d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        public int index;
3980d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
3990d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4000d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    // use factory method
4010d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private ICalendar() {
4020d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
4030d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4040d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    // TODO: get rid of this -- handle all of the parsing in one pass through
4050d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    // the text.
4060d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private static String normalizeText(String text) {
4070d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // it's supposed to be \r\n, but not everyone does that
4080d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        text = text.replaceAll("\r\n", "\n");
4090d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        text = text.replaceAll("\r", "\n");
4100d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4110d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // we deal with line folding, by replacing all "\n " strings
4120d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // with nothing.  The RFC specifies "\r\n " to be folded, but
4130d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // we handle "\n " and "\r " too because we can get those.
4140d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        text = text.replaceAll("\n ", "");
4150d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4160d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return text;
4170d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
4180d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4190d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
4200d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * Parses text into an iCalendar component.  Parses into the provided
4210d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * component, if not null, or parses into a new component.  In the latter
4220d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * case, expects a BEGIN as the first line.  Returns the provided or newly
4230d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * created top-level component.
4240d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     */
4250d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    // TODO: use an index into the text, so we can make this a recursive
4260d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    // function?
4270d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private static Component parseComponentImpl(Component component,
4280d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                                                String text)
4290d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            throws FormatException {
4300d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        Component current = component;
4310d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        ParserState state = new ParserState();
4320d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        state.index = 0;
4330d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4340d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // split into lines
4350d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String[] lines = text.split("\n");
4360d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4370d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // each line is of the format:
4380d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // name *(";" param) ":" value
4390d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        for (String line : lines) {
4400d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            try {
4410d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                current = parseLine(line, state, current);
4420d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                // if the provided component was null, we will return the root
4430d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                // NOTE: in this case, if the first line is not a BEGIN, a
4440d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                // FormatException will get thrown.
4450d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                if (component == null) {
4460d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    component = current;
4470d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                }
4480d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            } catch (FormatException fe) {
4490d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                if (false) {
4500d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    Log.v(TAG, "Cannot parse " + line, fe);
4510d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                }
4520d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                // for now, we ignore the parse error.  Google Calendar seems
4530d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                // to be emitting some misformatted iCalendar objects.
4540d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
4550d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            continue;
4560d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
4570d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return component;
4580d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
4590d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4600d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
4610d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * Parses a line into the provided component.  Creates a new component if
4620d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * the line is a BEGIN, adding the newly created component to the provided
4630d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * parent.  Returns whatever component is the current one (to which new
4640d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * properties will be added) in the parse.
4650d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     */
4660d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private static Component parseLine(String line, ParserState state,
4670d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                                       Component component)
4680d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            throws FormatException {
4690d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        state.line = line;
4700d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int len = state.line.length();
4710d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4720d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        // grab the name
4730d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        char c = 0;
4740d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        for (state.index = 0; state.index < len; ++state.index) {
4750d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            c = line.charAt(state.index);
4760d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (c == ';' || c == ':') {
4770d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                break;
4780d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
4790d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
4800d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String name = line.substring(0, state.index);
4810d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4820d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (component == null) {
4830d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (!Component.BEGIN.equals(name)) {
4840d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                throw new FormatException("Expected BEGIN");
4850d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
4860d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
4870d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
4880d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        Property property;
4890d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (Component.BEGIN.equals(name)) {
4900d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            // start a new component
4910d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            String componentName = extractValue(state);
4920d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            Component child = new Component(componentName, component);
4930d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (component != null) {
4940d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                component.addChild(child);
4950d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
4960d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return child;
4970d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        } else if (Component.END.equals(name)) {
4980d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            // finish the current component
4990d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            String componentName = extractValue(state);
5000d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (component == null ||
5010d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    !componentName.equals(component.getName())) {
5020d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                throw new FormatException("Unexpected END " + componentName);
5030d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
5040d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            return component.getParent();
5050d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        } else {
5060d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            property = new Property(name);
5070d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
5080d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
5090d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (c == ';') {
5100d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            Parameter parameter = null;
5110d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            while ((parameter = extractParameter(state)) != null) {
5120d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                property.addParameter(parameter);
5130d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
5140d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
5150d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String value = extractValue(state);
5160d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        property.setValue(value);
5170d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        component.addProperty(property);
5180d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return component;
5190d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
5200d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
5210d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
5220d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * Extracts the value ":..." on the current line.  The first character must
5230d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * be a ':'.
5240d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     */
5250d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private static String extractValue(ParserState state)
5260d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            throws FormatException {
5270d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String line = state.line;
5280d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (state.index >= line.length() || line.charAt(state.index) != ':') {
5290d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            throw new FormatException("Expected ':' before end of line in "
5300d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    + line);
5310d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
5320d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String value = line.substring(state.index + 1);
5330d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        state.index = line.length() - 1;
5340d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return value;
5350d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
5360d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
5370d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
5380d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * Extracts the next parameter from the line, if any.  If there are no more
5390d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * parameters, returns null.
5400d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     */
5410d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    private static Parameter extractParameter(ParserState state)
5420d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            throws FormatException {
5430d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        String text = state.line;
5440d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int len = text.length();
5450d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        Parameter parameter = null;
5460d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int startIndex = -1;
5470d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        int equalIndex = -1;
5480d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        while (state.index < len) {
5490d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            char c = text.charAt(state.index);
5500d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            if (c == ':') {
5510d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                if (parameter != null) {
5520d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    if (equalIndex == -1) {
5530d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                        throw new FormatException("Expected '=' within "
5540d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                                + "parameter in " + text);
5550d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    }
5560d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    parameter.value = text.substring(equalIndex + 1,
5570d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                                                     state.index);
5580d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                }
5590d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                return parameter; // may be null
5600d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            } else if (c == ';') {
5610d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                if (parameter != null) {
5620d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    if (equalIndex == -1) {
5630d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                        throw new FormatException("Expected '=' within "
5640d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                                + "parameter in " + text);
5650d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    }
5660d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    parameter.value = text.substring(equalIndex + 1,
5670d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                                                     state.index);
5680d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    return parameter;
5690d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                } else {
5700d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    parameter = new Parameter();
5710d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    startIndex = state.index;
5720d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                }
5730d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            } else if (c == '=') {
5740d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                equalIndex = state.index;
5750d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                if ((parameter == null) || (startIndex == -1)) {
5760d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    throw new FormatException("Expected ';' before '=' in "
5770d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                            + text);
5780d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                }
5790d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                parameter.name = text.substring(startIndex + 1, equalIndex);
5800d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            } else if (c == '"') {
5810d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                if (parameter == null) {
5820d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    throw new FormatException("Expected parameter before '\"' in " + text);
5830d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                }
5840d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                if (equalIndex == -1) {
5850d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    throw new FormatException("Expected '=' within parameter in " + text);
5860d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                }
5870d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                if (state.index > equalIndex + 1) {
5880d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    throw new FormatException("Parameter value cannot contain a '\"' in " + text);
5890d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                }
5900d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                final int endQuote = text.indexOf('"', state.index + 1);
5910d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                if (endQuote < 0) {
5920d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                    throw new FormatException("Expected closing '\"' in " + text);
5930d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                }
5940d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                parameter.value = text.substring(state.index + 1, endQuote);
5950d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                state.index = endQuote + 1;
5960d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden                return parameter;
5970d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            }
5980d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            ++state.index;
5990d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
6000d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        throw new FormatException("Expected ':' before end of line in " + text);
6010d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
6020d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
6030d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
6040d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * Parses the provided text into an iCalendar object.  The top-level
6050d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * component must be of type VCALENDAR.
6060d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @param text The text to be parsed.
6070d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @return The top-level VCALENDAR component.
6080d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @throws FormatException Thrown if the text could not be parsed into an
6090d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * iCalendar VCALENDAR object.
6100d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     */
6110d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public static Component parseCalendar(String text) throws FormatException {
6120d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        Component calendar = parseComponent(null, text);
6130d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (calendar == null || !Component.VCALENDAR.equals(calendar.getName())) {
6140d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            throw new FormatException("Expected " + Component.VCALENDAR);
6150d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
6160d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return calendar;
6170d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
6180d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
6190d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
6200d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * Parses the provided text into an iCalendar event.  The top-level
6210d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * component must be of type VEVENT.
6220d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @param text The text to be parsed.
6230d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @return The top-level VEVENT component.
6240d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @throws FormatException Thrown if the text could not be parsed into an
6250d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * iCalendar VEVENT.
6260d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     */
6270d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public static Component parseEvent(String text) throws FormatException {
6280d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        Component event = parseComponent(null, text);
6290d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        if (event == null || !Component.VEVENT.equals(event.getName())) {
6300d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden            throw new FormatException("Expected " + Component.VEVENT);
6310d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        }
6320d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return event;
6330d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
6340d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
6350d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
6360d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * Parses the provided text into an iCalendar component.
6370d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @param text The text to be parsed.
6380d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @return The top-level component.
6390d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @throws FormatException Thrown if the text could not be parsed into an
6400d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * iCalendar component.
6410d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     */
6420d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public static Component parseComponent(String text) throws FormatException {
6430d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return parseComponent(null, text);
6440d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
6450d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden
6460d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    /**
6470d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * Parses the provided text, adding to the provided component.
6480d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @param component The component to which the parsed iCalendar data should
6490d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * be added.
6500d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @param text The text to be parsed.
6510d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @return The top-level component.
6520d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * @throws FormatException Thrown if the text could not be parsed as an
6530d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     * iCalendar object.
6540d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden     */
6550d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    public static Component parseComponent(Component component, String text)
6560d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        throws FormatException {
6570d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        text = normalizeText(text);
6580d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden        return parseComponentImpl(component, text);
6590d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden    }
6600d3524562e330e74f150a17c4dc4dd66a0faae46Andy McFadden}
661