001 package biweekly.io.text; 002 003 import java.io.Closeable; 004 import java.io.File; 005 import java.io.FileNotFoundException; 006 import java.io.FileReader; 007 import java.io.IOException; 008 import java.io.InputStream; 009 import java.io.InputStreamReader; 010 import java.io.Reader; 011 import java.io.StringReader; 012 import java.util.ArrayList; 013 import java.util.HashMap; 014 import java.util.List; 015 import java.util.Map; 016 017 import biweekly.ICalendar; 018 import biweekly.component.ICalComponent; 019 import biweekly.component.marshaller.ComponentLibrary; 020 import biweekly.component.marshaller.ICalComponentMarshaller; 021 import biweekly.component.marshaller.RawComponentMarshaller; 022 import biweekly.io.CannotParseException; 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 import biweekly.property.marshaller.PropertyLibrary; 030 import biweekly.property.marshaller.RawPropertyMarshaller; 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 * 062 * <pre> 063 * Reader reader = ... 064 * ICalReader icalReader = new ICalReader(reader); 065 * ICalendar ical; 066 * while ((ical = icalReader.readNext()) != null){ 067 * ... 068 * } 069 * icalReader.close(); 070 * </pre> 071 * @author Michael Angstadt 072 */ 073 public class ICalReader implements Closeable { 074 private final List<String> warnings = new ArrayList<String>(); 075 private final Map<String, ICalPropertyMarshaller<? extends ICalProperty>> propertyMarshallers = new HashMap<String, ICalPropertyMarshaller<? extends ICalProperty>>(0); 076 private final Map<String, ICalComponentMarshaller<? extends ICalComponent>> componentMarshallers = new HashMap<String, ICalComponentMarshaller<? extends ICalComponent>>(0); 077 private final ICalRawReader reader; 078 079 /** 080 * Creates a reader that parses iCalendar objects from a string. 081 * @param string the string 082 */ 083 public ICalReader(String string) { 084 this(new StringReader(string)); 085 } 086 087 /** 088 * Creates a reader that parses iCalendar objects from an input stream. 089 * @param in the input stream 090 */ 091 public ICalReader(InputStream in) { 092 this(new InputStreamReader(in)); 093 } 094 095 /** 096 * Creates a reader that parses iCalendar objects from a file. 097 * @param file the file 098 * @throws FileNotFoundException if the file doesn't exist 099 */ 100 public ICalReader(File file) throws FileNotFoundException { 101 this(new FileReader(file)); 102 } 103 104 /** 105 * Creates a reader that parses iCalendar objects from a reader. 106 * @param reader the reader 107 */ 108 public ICalReader(Reader reader) { 109 this.reader = new ICalRawReader(reader); 110 } 111 112 /** 113 * Gets whether the reader will decode parameter values that use circumflex 114 * accent encoding (enabled by default). This escaping mechanism allows 115 * newlines and double quotes to be included in parameter values. 116 * @return true if circumflex accent decoding is enabled, false if not 117 * @see ICalRawReader#isCaretDecodingEnabled() 118 */ 119 public boolean isCaretDecodingEnabled() { 120 return reader.isCaretDecodingEnabled(); 121 } 122 123 /** 124 * Sets whether the reader will decode parameter values that use circumflex 125 * accent encoding (enabled by default). This escaping mechanism allows 126 * newlines and double quotes to be included in parameter values. 127 * @param enable true to use circumflex accent decoding, false not to 128 * @see ICalRawReader#setCaretDecodingEnabled(boolean) 129 */ 130 public void setCaretDecodingEnabled(boolean enable) { 131 reader.setCaretDecodingEnabled(enable); 132 } 133 134 /** 135 * Registers a marshaller for an experimental property. 136 * @param marshaller the marshaller to register 137 */ 138 public void registerMarshaller(ICalPropertyMarshaller<? extends ICalProperty> marshaller) { 139 propertyMarshallers.put(marshaller.getPropertyName().toUpperCase(), marshaller); 140 } 141 142 /** 143 * Registers a marshaller for an experimental component. 144 * @param marshaller the marshaller to register 145 */ 146 public void registerMarshaller(ICalComponentMarshaller<? extends ICalComponent> marshaller) { 147 componentMarshallers.put(marshaller.getComponentName().toUpperCase(), marshaller); 148 } 149 150 /** 151 * Gets the warnings from the last iCalendar object that was unmarshalled. 152 * This list is reset every time a new iCalendar object is read. 153 * @return the warnings or empty list if there were no warnings 154 */ 155 public List<String> getWarnings() { 156 return new ArrayList<String>(warnings); 157 } 158 159 /** 160 * Reads the next iCalendar object. 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 public ICalendar readNext() throws IOException { 165 if (reader.eof()) { 166 return null; 167 } 168 169 warnings.clear(); 170 171 ICalDataStreamListenerImpl listener = new ICalDataStreamListenerImpl(); 172 reader.start(listener); 173 174 if (!listener.dataWasRead) { 175 //EOF was reached without reading anything 176 return null; 177 } 178 179 ICalendar ical; 180 if (listener.orphanedComponents.isEmpty()) { 181 //there were no components in the iCalendar object 182 ical = new ICalendar(); 183 ical.getProperties().clear(); //clear properties that are created in the constructor 184 } else { 185 ICalComponent first = listener.orphanedComponents.get(0); 186 if (first instanceof ICalendar) { 187 //this is the code-path that valid iCalendar objects should reach 188 ical = (ICalendar) first; 189 } else { 190 ical = new ICalendar(); 191 ical.getProperties().clear(); //clear properties that are created in the constructor 192 for (ICalComponent component : listener.orphanedComponents) { 193 ical.addComponent(component); 194 } 195 } 196 } 197 198 //add any properties that were not part of a component (will never happen if the iCalendar object is valid) 199 for (ICalProperty property : listener.orphanedProperties) { 200 ical.addProperty(property); 201 } 202 203 return ical; 204 } 205 206 /** 207 * Finds a component marshaller. 208 * @param componentName the name of the component 209 * @return the component marshallerd 210 */ 211 private ICalComponentMarshaller<? extends ICalComponent> findComponentMarshaller(String componentName) { 212 ICalComponentMarshaller<? extends ICalComponent> m = componentMarshallers.get(componentName.toUpperCase()); 213 if (m == null) { 214 m = ComponentLibrary.getMarshaller(componentName); 215 if (m == null) { 216 m = new RawComponentMarshaller(componentName); 217 } 218 } 219 return m; 220 } 221 222 /** 223 * Finds a property marshaller. 224 * @param propertyName the name of the property 225 * @return the property marshaller 226 */ 227 private ICalPropertyMarshaller<? extends ICalProperty> findPropertyMarshaller(String propertyName) { 228 ICalPropertyMarshaller<? extends ICalProperty> m = propertyMarshallers.get(propertyName); 229 if (m == null) { 230 m = PropertyLibrary.getMarshaller(propertyName); 231 if (m == null) { 232 m = new RawPropertyMarshaller(propertyName); 233 } 234 } 235 return m; 236 } 237 238 //TODO how to unmarshal the alarm components (a different class should be created, depending on the ACTION property) 239 //TODO buffer properties in a list before the component class is created 240 private class ICalDataStreamListenerImpl implements ICalRawReader.ICalDataStreamListener { 241 private final String icalComponentName = ComponentLibrary.getMarshaller(ICalendar.class).getComponentName(); 242 243 private List<ICalProperty> orphanedProperties = new ArrayList<ICalProperty>(); 244 private List<ICalComponent> orphanedComponents = new ArrayList<ICalComponent>(); 245 246 private List<ICalComponent> componentStack = new ArrayList<ICalComponent>(); 247 private List<String> componentNamesStack = new ArrayList<String>(); 248 private boolean dataWasRead = false; 249 250 public void beginComponent(String name) { 251 dataWasRead = true; 252 253 ICalComponent parentComponent = getCurrentComponent(); 254 255 ICalComponentMarshaller<? extends ICalComponent> m = findComponentMarshaller(name); 256 ICalComponent component = m.emptyInstance(); 257 componentStack.add(component); 258 componentNamesStack.add(name); 259 260 if (parentComponent == null) { 261 orphanedComponents.add(component); 262 } else { 263 parentComponent.addComponent(component); 264 } 265 } 266 267 public void readProperty(String name, ICalParameters parameters, String value) { 268 dataWasRead = true; 269 270 ICalPropertyMarshaller<? extends ICalProperty> m = findPropertyMarshaller(name); 271 ICalProperty property = null; 272 try { 273 Result<? extends ICalProperty> result = m.parseText(value, parameters); 274 275 for (String warning : result.getWarnings()) { 276 addWarning(warning, name); 277 } 278 279 property = result.getValue(); 280 } catch (SkipMeException e) { 281 if (e.getMessage() == null) { 282 addWarning("Property has requested that it be skipped.", name); 283 } else { 284 addWarning("Property has requested that it be skipped: " + e.getMessage(), name); 285 } 286 } catch (CannotParseException e) { 287 if (e.getMessage() == null) { 288 addWarning("Property value could not be unmarshalled: " + value, name); 289 } else { 290 addWarning("Property value could not be unmarshalled.\n Value: " + value + "\n Reason: " + e.getMessage(), name); 291 } 292 property = new RawProperty(name, value); 293 } 294 295 if (property != null) { 296 ICalComponent parentComponent = getCurrentComponent(); 297 if (parentComponent == null) { 298 orphanedProperties.add(property); 299 } else { 300 parentComponent.addProperty(property); 301 } 302 } 303 } 304 305 public void endComponent(String name) { 306 //stop reading when "END:VCALENDAR" is reached 307 if (icalComponentName.equalsIgnoreCase(name)) { 308 throw new ICalRawReader.StopReadingException(); 309 } 310 311 //find the component that this END property matches up with 312 int popIndex = -1; 313 for (int i = componentStack.size() - 1; i >= 0; i--) { 314 String n = componentNamesStack.get(i); 315 if (n.equalsIgnoreCase(name)) { 316 popIndex = i; 317 break; 318 } 319 } 320 if (popIndex == -1) { 321 //END property does not match up with any BEGIN properties, so ignore 322 addWarning("Ignoring END property that does not match up with any BEGIN properties: " + name, "END"); 323 return; 324 } 325 326 componentStack = componentStack.subList(0, popIndex); 327 componentNamesStack = componentNamesStack.subList(0, popIndex); 328 } 329 330 public void invalidLine(String line) { 331 addWarning("Skipping malformed line: \"" + line + "\""); 332 } 333 334 private ICalComponent getCurrentComponent() { 335 if (componentStack.isEmpty()) { 336 return null; 337 } 338 return componentStack.get(componentStack.size() - 1); 339 } 340 } 341 342 private void addWarning(String message) { 343 addWarning(message, null); 344 } 345 346 private void addWarning(String message, String propertyName) { 347 addWarning(message, propertyName, reader.getLineNum()); 348 } 349 350 private void addWarning(String message, String propertyName, int lineNum) { 351 StringBuilder sb = new StringBuilder(); 352 sb.append("Line ").append(lineNum); 353 if (propertyName != null) { 354 sb.append(" (").append(propertyName).append(" property)"); 355 } 356 sb.append(": ").append(message); 357 358 warnings.add(sb.toString()); 359 } 360 361 /** 362 * Closes the underlying {@link Reader} object. 363 */ 364 //@Override 365 public void close() throws IOException { 366 reader.close(); 367 } 368 }