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}