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