001package biweekly.io.text; 002 003import static biweekly.util.IOUtils.utf8Reader; 004 005import java.io.Closeable; 006import java.io.File; 007import java.io.FileNotFoundException; 008import java.io.IOException; 009import java.io.InputStream; 010import java.io.Reader; 011import java.io.StringReader; 012import java.util.ArrayList; 013import java.util.List; 014import java.util.Map; 015 016import biweekly.ICalDataType; 017import biweekly.ICalendar; 018import biweekly.Warning; 019import biweekly.component.ICalComponent; 020import biweekly.io.CannotParseException; 021import biweekly.io.ParseWarnings; 022import biweekly.io.SkipMeException; 023import biweekly.io.scribe.ScribeIndex; 024import biweekly.io.scribe.component.ICalComponentScribe; 025import biweekly.io.scribe.component.ICalendarScribe; 026import biweekly.io.scribe.property.ICalPropertyScribe; 027import biweekly.io.scribe.property.ICalPropertyScribe.Result; 028import biweekly.io.scribe.property.RawPropertyScribe; 029import biweekly.parameter.ICalParameters; 030import biweekly.property.ICalProperty; 031 032/* 033 Copyright (c) 2013, Michael Angstadt 034 All rights reserved. 035 036 Redistribution and use in source and binary forms, with or without 037 modification, are permitted provided that the following conditions are met: 038 039 1. Redistributions of source code must retain the above copyright notice, this 040 list of conditions and the following disclaimer. 041 2. Redistributions in binary form must reproduce the above copyright notice, 042 this list of conditions and the following disclaimer in the documentation 043 and/or other materials provided with the distribution. 044 045 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 046 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 047 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 048 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 049 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 050 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 051 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 052 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 053 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 054 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 055 */ 056 057/** 058 * <p> 059 * Parses {@link ICalendar} objects from an iCalendar data stream. 060 * </p> 061 * <p> 062 * <b>Example:</b> 063 * 064 * <pre class="brush:java"> 065 * InputStream in = ... 066 * ICalReader icalReader = new ICalReader(in); 067 * ICalendar ical; 068 * while ((ical = icalReader.readNext()) != null){ 069 * ... 070 * } 071 * icalReader.close(); 072 * </pre> 073 * 074 * </p> 075 * @author Michael Angstadt 076 * @see <a href="http://tools.ietf.org/html/rfc5545">RFC 5545</a> 077 */ 078public class ICalReader implements Closeable { 079 private static final ICalendarScribe icalMarshaller = ScribeIndex.getICalendarScribe(); 080 private static final String icalComponentName = icalMarshaller.getComponentName(); 081 private final ParseWarnings warnings = new ParseWarnings(); 082 private ScribeIndex index = new ScribeIndex(); 083 private final ICalRawReader reader; 084 085 /** 086 * Creates a reader that parses iCalendar objects from a string. 087 * @param string the string 088 */ 089 public ICalReader(String string) { 090 this(new StringReader(string)); 091 } 092 093 /** 094 * Creates a reader that parses iCalendar objects from an input stream. 095 * @param in the input stream 096 */ 097 public ICalReader(InputStream in) { 098 this(utf8Reader(in)); 099 } 100 101 /** 102 * Creates a reader that parses iCalendar objects from a file. 103 * @param file the file 104 * @throws FileNotFoundException if the file doesn't exist 105 */ 106 public ICalReader(File file) throws FileNotFoundException { 107 this(utf8Reader(file)); 108 } 109 110 /** 111 * Creates a reader that parses iCalendar objects from a reader. 112 * @param reader the reader 113 */ 114 public ICalReader(Reader reader) { 115 this.reader = new ICalRawReader(reader); 116 } 117 118 /** 119 * Gets whether the reader will decode parameter values that use circumflex 120 * accent encoding (enabled by default). This escaping mechanism allows 121 * newlines and double quotes to be included in parameter values. 122 * @return true if circumflex accent decoding is enabled, false if not 123 * @see ICalRawReader#isCaretDecodingEnabled() 124 */ 125 public boolean isCaretDecodingEnabled() { 126 return reader.isCaretDecodingEnabled(); 127 } 128 129 /** 130 * Sets whether the reader will decode parameter values that use circumflex 131 * accent encoding (enabled by default). This escaping mechanism allows 132 * newlines and double quotes to be included in parameter values. 133 * @param enable true to use circumflex accent decoding, false not to 134 * @see ICalRawReader#setCaretDecodingEnabled(boolean) 135 */ 136 public void setCaretDecodingEnabled(boolean enable) { 137 reader.setCaretDecodingEnabled(enable); 138 } 139 140 /** 141 * <p> 142 * Registers an experimental property scribe. Can also be used to override 143 * the scribe of a standard property (such as DTSTART). Calling this method 144 * is the same as calling: 145 * </p> 146 * <p> 147 * {@code getScribeIndex().register(scribe)}. 148 * </p> 149 * @param scribe the scribe to register 150 */ 151 public void registerScribe(ICalPropertyScribe<? extends ICalProperty> scribe) { 152 index.register(scribe); 153 } 154 155 /** 156 * <p> 157 * Registers an experimental component scribe. Can also be used to override 158 * the scribe of a standard component (such as VEVENT). Calling this method 159 * is the same as calling: 160 * </p> 161 * <p> 162 * {@code getScribeIndex().register(scribe)}. 163 * </p> 164 * @param scribe the scribe to register 165 */ 166 public void registerScribe(ICalComponentScribe<? extends ICalComponent> scribe) { 167 index.register(scribe); 168 } 169 170 /** 171 * Gets the object that manages the component/property scribes. 172 * @return the scribe index 173 */ 174 public ScribeIndex getScribeIndex() { 175 return index; 176 } 177 178 /** 179 * Sets the object that manages the component/property scribes. 180 * @param index the scribe index 181 */ 182 public void setScribeIndex(ScribeIndex index) { 183 this.index = index; 184 } 185 186 /** 187 * Gets the warnings from the last iCalendar object that was unmarshalled. 188 * This list is reset every time a new iCalendar object is read. 189 * @return the warnings or empty list if there were no warnings 190 */ 191 public List<String> getWarnings() { 192 return warnings.copy(); 193 } 194 195 /** 196 * Reads the next iCalendar object. 197 * @return the next iCalendar object or null if there are no more 198 * @throws IOException if there's a problem reading from the stream 199 */ 200 public ICalendar readNext() throws IOException { 201 warnings.clear(); 202 203 boolean dataWasRead = false; 204 205 List<ICalProperty> orphanedProperties = new ArrayList<ICalProperty>(); 206 List<ICalComponent> orphanedComponents = new ArrayList<ICalComponent>(); 207 208 List<ICalComponent> componentStack = new ArrayList<ICalComponent>(); 209 List<String> componentNamesStack = new ArrayList<String>(); 210 211 while (true) { 212 //read next line 213 ICalRawLine line; 214 try { 215 line = reader.readLine(); 216 } catch (ICalParseException e) { 217 warnings.add(reader.getLineNum(), null, 3, e.getLine()); 218 continue; 219 } 220 221 //EOF 222 if (line == null) { 223 break; 224 } 225 226 String propertyName = line.getName(); 227 228 if ("BEGIN".equalsIgnoreCase(propertyName)) { 229 String componentName = line.getValue(); 230 dataWasRead = true; 231 232 ICalComponent parentComponent = componentStack.isEmpty() ? null : componentStack.get(componentStack.size() - 1); 233 234 ICalComponentScribe<? extends ICalComponent> marshaller = index.getComponentScribe(componentName); 235 ICalComponent component = marshaller.emptyInstance(); 236 componentStack.add(component); 237 componentNamesStack.add(componentName); 238 239 if (parentComponent == null) { 240 orphanedComponents.add(component); 241 } else { 242 parentComponent.addComponent(component); 243 } 244 245 continue; 246 } 247 248 if ("END".equalsIgnoreCase(propertyName)) { 249 String componentName = line.getValue(); 250 251 //stop reading when "END:VCALENDAR" is reached 252 if (icalComponentName.equalsIgnoreCase(componentName)) { 253 break; 254 } 255 256 //find the component that this END property matches up with 257 int popIndex = -1; 258 for (int i = componentStack.size() - 1; i >= 0; i--) { 259 String name = componentNamesStack.get(i); 260 if (name.equalsIgnoreCase(componentName)) { 261 popIndex = i; 262 break; 263 } 264 } 265 if (popIndex == -1) { 266 //END property does not match up with any BEGIN properties, so ignore 267 warnings.add(reader.getLineNum(), "END", 2); 268 } else { 269 componentStack.subList(popIndex, componentStack.size()).clear(); 270 componentNamesStack.subList(popIndex, componentNamesStack.size()).clear(); 271 } 272 273 continue; 274 } 275 276 dataWasRead = true; 277 278 //check for value-less parameters 279 ICalParameters parameters = line.getParameters(); 280 for (Map.Entry<String, List<String>> entry : parameters) { 281 List<String> paramValues = entry.getValue(); 282 for (String value : paramValues) { 283 if (value == null) { 284 String paramName = entry.getKey(); 285 warnings.add(reader.getLineNum(), propertyName, 4, paramName); 286 break; 287 } 288 } 289 } 290 291 ICalPropertyScribe<? extends ICalProperty> marshaller = index.getPropertyScribe(propertyName); 292 293 //get the data type 294 ICalDataType dataType = parameters.getValue(); 295 if (dataType == null) { 296 //use the default data type if there is no VALUE parameter 297 dataType = marshaller.getDefaultDataType(); 298 } else { 299 //remove VALUE parameter if it is set 300 parameters.setValue(null); 301 } 302 303 //marshal the property 304 ICalProperty property = null; 305 String value = line.getValue(); 306 try { 307 Result<? extends ICalProperty> result = marshaller.parseText(value, dataType, parameters); 308 for (Warning warning : result.getWarnings()) { 309 warnings.add(reader.getLineNum(), propertyName, warning); 310 } 311 property = result.getProperty(); 312 } catch (SkipMeException e) { 313 warnings.add(reader.getLineNum(), propertyName, 0, e.getMessage()); 314 continue; 315 } catch (CannotParseException e) { 316 warnings.add(reader.getLineNum(), propertyName, 1, value, e.getMessage()); 317 318 Result<? extends ICalProperty> result = new RawPropertyScribe(propertyName).parseText(value, dataType, parameters); 319 property = result.getProperty(); 320 } 321 322 //add the property to its component 323 if (componentStack.isEmpty()) { 324 orphanedProperties.add(property); 325 } else { 326 ICalComponent parentComponent = componentStack.get(componentStack.size() - 1); 327 parentComponent.addProperty(property); 328 } 329 } 330 331 if (!dataWasRead) { 332 //EOF was reached without reading anything 333 return null; 334 } 335 336 ICalendar ical; 337 if (orphanedComponents.isEmpty()) { 338 //there were no components in the iCalendar object 339 ical = icalMarshaller.emptyInstance(); 340 } else { 341 ICalComponent first = orphanedComponents.get(0); 342 if (first instanceof ICalendar) { 343 //this is the code-path that valid iCalendar objects should reach 344 ical = (ICalendar) first; 345 } else { 346 ical = icalMarshaller.emptyInstance(); 347 for (ICalComponent component : orphanedComponents) { 348 ical.addComponent(component); 349 } 350 } 351 } 352 353 //add any properties that were not part of a component (will never happen if the iCalendar object is valid) 354 for (ICalProperty property : orphanedProperties) { 355 ical.addProperty(property); 356 } 357 358 return ical; 359 } 360 361 /** 362 * Closes the underlying {@link Reader} object. 363 */ 364 public void close() throws IOException { 365 reader.close(); 366 } 367}