001package biweekly.io;
002
003import static biweekly.io.DataModelConverter.convert;
004
005import java.io.Closeable;
006import java.io.IOException;
007import java.util.ArrayList;
008import java.util.Date;
009import java.util.List;
010import java.util.Map;
011import java.util.TimeZone;
012
013import biweekly.ICalendar;
014import biweekly.component.ICalComponent;
015import biweekly.component.VTimezone;
016import biweekly.io.ParseContext.TimezonedDate;
017import biweekly.io.scribe.ScribeIndex;
018import biweekly.io.scribe.component.ICalComponentScribe;
019import biweekly.io.scribe.property.ICalPropertyScribe;
020import biweekly.property.Daylight;
021import biweekly.property.ICalProperty;
022import biweekly.property.Timezone;
023import biweekly.property.TimezoneId;
024import biweekly.util.ICalDate;
025import biweekly.util.ICalDateFormat;
026
027/*
028 Copyright (c) 2013-2015, Michael Angstadt
029 All rights reserved.
030
031 Redistribution and use in source and binary forms, with or without
032 modification, are permitted provided that the following conditions are met: 
033
034 1. Redistributions of source code must retain the above copyright notice, this
035 list of conditions and the following disclaimer. 
036 2. Redistributions in binary form must reproduce the above copyright notice,
037 this list of conditions and the following disclaimer in the documentation
038 and/or other materials provided with the distribution. 
039
040 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
041 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
042 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
043 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
044 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
045 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
046 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
047 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
048 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
049 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
050 */
051
052/**
053 * Parses iCalendar objects from a data stream.
054 * @author Michael Angstadt
055 */
056public abstract class StreamReader implements Closeable {
057        protected final ParseWarnings warnings = new ParseWarnings();
058        protected TimezoneInfo tzinfo;
059        protected ScribeIndex index = new ScribeIndex();
060        protected ParseContext context;
061
062        /**
063         * <p>
064         * Registers an experimental property scribe. Can also be used to override
065         * the scribe of a standard property (such as DTSTART). Calling this method
066         * is the same as calling:
067         * </p>
068         * <p>
069         * {@code getScribeIndex().register(scribe)}.
070         * </p>
071         * @param scribe the scribe to register
072         */
073        public void registerScribe(ICalPropertyScribe<? extends ICalProperty> scribe) {
074                index.register(scribe);
075        }
076
077        /**
078         * <p>
079         * Registers an experimental component scribe. Can also be used to override
080         * the scribe of a standard component (such as VEVENT). Calling this method
081         * is the same as calling:
082         * </p>
083         * <p>
084         * {@code getScribeIndex().register(scribe)}.
085         * </p>
086         * @param scribe the scribe to register
087         */
088        public void registerScribe(ICalComponentScribe<? extends ICalComponent> scribe) {
089                index.register(scribe);
090        }
091
092        /**
093         * Gets the object that manages the component/property scribes.
094         * @return the scribe index
095         */
096        public ScribeIndex getScribeIndex() {
097                return index;
098        }
099
100        /**
101         * Sets the object that manages the component/property scribes.
102         * @param index the scribe index
103         */
104        public void setScribeIndex(ScribeIndex index) {
105                this.index = index;
106        }
107
108        /**
109         * Gets the warnings from the last iCalendar object that was read. This list
110         * is reset every time a new iCalendar object is read.
111         * @return the warnings or empty list if there were no warnings
112         */
113        public List<String> getWarnings() {
114                return warnings.copy();
115        }
116
117        /**
118         * Gets the timezone info of the last iCalendar object that was read.
119         * @return the timezone info
120         */
121        public TimezoneInfo getTimezoneInfo() {
122                return tzinfo;
123        }
124
125        /**
126         * Reads all iCalendar objects from the data stream.
127         * @return the iCalendar objects
128         * @throws IOException if there's a problem reading from the stream
129         */
130        public List<ICalendar> readAll() throws IOException {
131                List<ICalendar> icals = new ArrayList<ICalendar>();
132                ICalendar ical = null;
133                while ((ical = readNext()) != null) {
134                        icals.add(ical);
135                }
136                return icals;
137        }
138
139        /**
140         * Reads the next iCalendar object from the data stream.
141         * @return the next iCalendar object or null if there are no more
142         * @throws IOException if there's a problem reading from the stream
143         */
144        public ICalendar readNext() throws IOException {
145                warnings.clear();
146                context = new ParseContext();
147                tzinfo = new TimezoneInfo();
148
149                ICalendar ical = _readNext();
150                if (ical == null) {
151                        return null;
152                }
153
154                ical.setVersion(context.getVersion());
155                handleTimezones(ical);
156                return ical;
157        }
158
159        /**
160         * Reads the next iCalendar object from the data stream.
161         * @return the next iCalendar object or null if there are no more
162         * @throws IOException if there's a problem reading from the stream
163         */
164        protected abstract ICalendar _readNext() throws IOException;
165
166        private void handleTimezones(ICalendar ical) {
167                //convert vCalendar DAYLIGHT and TZ properties to a VTIMEZONE component
168                VTimezone vcalComponent;
169                {
170                        List<Daylight> daylights = ical.getProperties(Daylight.class);
171                        Timezone tz = ical.getProperty(Timezone.class);
172
173                        vcalComponent = convert(daylights, tz);
174                        if (vcalComponent != null) {
175                                TimeZone timezone = new ICalTimeZone(vcalComponent);
176                                tzinfo.assign(vcalComponent, timezone);
177                                tzinfo.setDefaultTimeZone(timezone);
178                        }
179
180                        ical.removeProperties(Daylight.class);
181                        ical.removeProperties(Timezone.class);
182                }
183
184                //assign a TimeZone object to each VTIMEZONE component.
185                List<ICalComponent> toKeep = new ArrayList<ICalComponent>(0);
186                for (VTimezone component : ical.getComponents(VTimezone.class)) {
187                        //make sure the component has an ID
188                        TimezoneId id = component.getTimezoneId();
189                        if (id == null || id.getValue() == null) {
190                                warnings.add(null, null, 39);
191                                toKeep.add(component);
192                                continue;
193                        }
194
195                        TimeZone timezone = new ICalTimeZone(component);
196                        tzinfo.assign(component, timezone);
197                }
198
199                //remove the VTIMEZONE components from the iCalendar object
200                if (toKeep.isEmpty()) {
201                        ical.removeComponents(VTimezone.class);
202                } else {
203                        //keep the VTIMEZONE components that don't have IDs
204                        ical.getComponents().replace(VTimezone.class, toKeep);
205                }
206
207                if (vcalComponent != null) {
208                        //vCal: parse floating dates according to the DAYLIGHT and TZ properties (which were converted to a VTIMEZONE component)
209                        TimeZone timezone = tzinfo.getTimeZoneByComponent(vcalComponent);
210                        for (TimezonedDate timezonedDate : context.getFloatingDates()) {
211                                ICalDate date = timezonedDate.getDate();
212
213                                //parse its raw date components under its real timezone
214                                Date realDate = date.getRawComponents().toDate(timezone);
215
216                                //update the ICalDate object with the new timestamp
217                                date.setTime(realDate.getTime());
218                        }
219                } else {
220                        for (TimezonedDate timezonedDate : context.getFloatingDates()) {
221                                tzinfo.setFloating(timezonedDate.getProperty(), true);
222                        }
223                }
224
225                for (Map.Entry<String, List<TimezonedDate>> entry : context.getTimezonedDates()) {
226                        //find the VTIMEZONE component with the given TZID
227                        String tzid = entry.getKey();
228
229                        boolean solidus = tzid.startsWith("/");
230                        TimeZone timezone;
231                        if (solidus) {
232                                //treat the TZID parameter value as an Olsen timezone ID
233                                timezone = ICalDateFormat.parseTimeZoneId(tzid.substring(1));
234                                if (timezone == null) {
235                                        //timezone could not be determined
236                                        warnings.add(null, null, 38, tzid);
237                                        continue;
238                                }
239                        } else {
240                                timezone = tzinfo.getTimeZoneById(tzid);
241                                if (timezone == null) {
242                                        //A VTIMEZONE component couldn't found
243                                        //so treat the TZID parameter value as an Olsen timezone ID
244                                        timezone = ICalDateFormat.parseTimeZoneId(tzid);
245                                        if (timezone == null) {
246                                                //timezone could not be determined
247                                                warnings.add(null, null, 38, tzid);
248                                                continue;
249                                        }
250
251                                        warnings.add(null, null, 37, tzid);
252                                }
253                        }
254
255                        List<TimezonedDate> timezonedDates = entry.getValue();
256                        for (TimezonedDate timezonedDate : timezonedDates) {
257                                //assign the property to the timezone
258                                ICalProperty property = timezonedDate.getProperty();
259                                tzinfo.setTimeZoneReader(property, timezone, solidus);
260
261                                ICalDate date = timezonedDate.getDate();
262
263                                //parse its raw date components under its real timezone
264                                Date realDate = date.getRawComponents().toDate(timezone);
265
266                                //update the Date object with the new timestamp
267                                date.setTime(realDate.getTime());
268
269                                //remove the TZID parameter
270                                property.getParameters().setTimezoneId(null);
271                        }
272                }
273        }
274}