001package biweekly.io.xml; 002 003import static biweekly.io.xml.XCalNamespaceContext.XCAL_NS; 004import static biweekly.io.xml.XCalQNames.COMPONENTS; 005import static biweekly.io.xml.XCalQNames.ICALENDAR; 006import static biweekly.io.xml.XCalQNames.PARAMETERS; 007import static biweekly.io.xml.XCalQNames.PROPERTIES; 008import static biweekly.io.xml.XCalQNames.VCALENDAR; 009 010import java.io.Closeable; 011import java.io.File; 012import java.io.FileInputStream; 013import java.io.FileNotFoundException; 014import java.io.IOException; 015import java.io.InputStream; 016import java.io.Reader; 017import java.io.StringReader; 018import java.util.ArrayList; 019import java.util.LinkedList; 020import java.util.List; 021import java.util.concurrent.ArrayBlockingQueue; 022import java.util.concurrent.BlockingQueue; 023 024import javax.xml.namespace.QName; 025import javax.xml.transform.ErrorListener; 026import javax.xml.transform.Source; 027import javax.xml.transform.Transformer; 028import javax.xml.transform.TransformerConfigurationException; 029import javax.xml.transform.TransformerException; 030import javax.xml.transform.TransformerFactory; 031import javax.xml.transform.dom.DOMSource; 032import javax.xml.transform.sax.SAXResult; 033import javax.xml.transform.stream.StreamSource; 034 035import org.w3c.dom.Document; 036import org.w3c.dom.Element; 037import org.w3c.dom.Node; 038import org.xml.sax.Attributes; 039import org.xml.sax.SAXException; 040import org.xml.sax.helpers.DefaultHandler; 041 042import biweekly.ICalVersion; 043import biweekly.ICalendar; 044import biweekly.Warning; 045import biweekly.component.ICalComponent; 046import biweekly.io.CannotParseException; 047import biweekly.io.ParseContext; 048import biweekly.io.SkipMeException; 049import biweekly.io.StreamReader; 050import biweekly.io.TimezoneInfo; 051import biweekly.io.scribe.component.ICalComponentScribe; 052import biweekly.io.scribe.property.ICalPropertyScribe; 053import biweekly.parameter.ICalParameters; 054import biweekly.property.ICalProperty; 055import biweekly.property.Version; 056import biweekly.property.Xml; 057import biweekly.util.XmlUtils; 058 059/* 060 Copyright (c) 2013-2015, Michael Angstadt 061 All rights reserved. 062 063 Redistribution and use in source and binary forms, with or without 064 modification, are permitted provided that the following conditions are met: 065 066 1. Redistributions of source code must retain the above copyright notice, this 067 list of conditions and the following disclaimer. 068 2. Redistributions in binary form must reproduce the above copyright notice, 069 this list of conditions and the following disclaimer in the documentation 070 and/or other materials provided with the distribution. 071 072 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 073 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 074 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 075 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 076 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 077 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 078 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 079 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 080 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 081 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 082 083 The views and conclusions contained in the software and documentation are those 084 of the authors and should not be interpreted as representing official policies, 085 either expressed or implied, of the FreeBSD Project. 086 */ 087 088/** 089 * <p> 090 * Reads xCals (XML-encoded iCalendar objects) in a streaming fashion. 091 * </p> 092 * <p> 093 * <b>Example:</b> 094 * 095 * <pre class="brush:java"> 096 * File file = new File("icals.xml"); 097 * XCalReader reader = null; 098 * try { 099 * reader = new XCalReader(file); 100 * ICalendar ical; 101 * while ((ical = reader.readNext()) != null){ 102 * ... 103 * } 104 * } finally { 105 * if (reader != null) reader.close(); 106 * } 107 * </pre> 108 * 109 * </p> 110 * 111 * <p> 112 * <b>Getting timezone information:</b> 113 * 114 * <pre class="brush:java"> 115 * XCalReader reader = ... 116 * ICalendar ical = reader.readNext(); 117 * TimezoneInfo tzinfo = reader.getTimezoneInfo(); 118 * 119 * //get the VTIMEZONE components that were parsed 120 * //the VTIMEZONE components will NOT be in the ICalendar object 121 * Collection<VTimezone> vtimezones = tzinfo.getComponents(); 122 * 123 * //get the timezone that a property was originally formatted in 124 * DateStart dtstart = ical.getEvents().get(0).getDateStart(); 125 * TimeZone tz = tzinfo.getTimeZone(dtstart); 126 * </pre> 127 * 128 * </p> 129 * @author Michael Angstadt 130 * @see <a href="http://tools.ietf.org/html/rfc6321">RFC 6321</a> 131 */ 132public class XCalReader extends StreamReader { 133 private final Source source; 134 private final Closeable stream; 135 136 private volatile ICalendar readICal; 137 private volatile TransformerException thrown; 138 139 private final ReadThread thread = new ReadThread(); 140 private final Object lock = new Object(); 141 private final BlockingQueue<Object> readerBlock = new ArrayBlockingQueue<Object>(1); 142 private final BlockingQueue<Object> threadBlock = new ArrayBlockingQueue<Object>(1); 143 144 /** 145 * @param str the string to read from 146 */ 147 public XCalReader(String str) { 148 this(new StringReader(str)); 149 } 150 151 /** 152 * @param in the input stream to read from 153 */ 154 public XCalReader(InputStream in) { 155 source = new StreamSource(in); 156 stream = in; 157 } 158 159 /** 160 * @param file the file to read from 161 * @throws FileNotFoundException if the file doesn't exist 162 */ 163 public XCalReader(File file) throws FileNotFoundException { 164 this(new FileInputStream(file)); 165 } 166 167 /** 168 * @param reader the reader to read from 169 */ 170 public XCalReader(Reader reader) { 171 source = new StreamSource(reader); 172 stream = reader; 173 } 174 175 /** 176 * @param node the DOM node to read from 177 */ 178 public XCalReader(Node node) { 179 source = new DOMSource(node); 180 stream = null; 181 } 182 183 @Override 184 protected ICalendar _readNext() throws IOException { 185 readICal = null; 186 warnings.clear(); 187 context = new ParseContext(); 188 tzinfo = new TimezoneInfo(); 189 thrown = null; 190 191 if (!thread.started) { 192 thread.start(); 193 } else { 194 if (thread.finished || thread.closed) { 195 return null; 196 } 197 198 try { 199 threadBlock.put(lock); 200 } catch (InterruptedException e) { 201 return null; 202 } 203 } 204 205 //wait until thread reads xCard 206 try { 207 readerBlock.take(); 208 } catch (InterruptedException e) { 209 return null; 210 } 211 212 if (thrown != null) { 213 throw new IOException(thrown); 214 } 215 216 return readICal; 217 } 218 219 private class ReadThread extends Thread { 220 private final SAXResult result; 221 private final Transformer transformer; 222 private volatile boolean finished = false, started = false, closed = false; 223 224 public ReadThread() { 225 setName(getClass().getSimpleName()); 226 227 //create the transformer 228 try { 229 transformer = TransformerFactory.newInstance().newTransformer(); 230 } catch (TransformerConfigurationException e) { 231 //shouldn't be thrown because it's a simple configuration 232 throw new RuntimeException(e); 233 } 234 235 //prevent error messages from being printed to stderr 236 transformer.setErrorListener(new NoOpErrorListener()); 237 238 result = new SAXResult(new ContentHandlerImpl()); 239 } 240 241 @Override 242 public void run() { 243 started = true; 244 245 try { 246 transformer.transform(source, result); 247 } catch (TransformerException e) { 248 if (!thread.closed) { 249 thrown = e; 250 } 251 } finally { 252 finished = true; 253 try { 254 readerBlock.put(lock); 255 } catch (InterruptedException e) { 256 //ignore 257 } 258 } 259 } 260 } 261 262 private class ContentHandlerImpl extends DefaultHandler { 263 private final Document DOC = XmlUtils.createDocument(); 264 private final XCalStructure structure = new XCalStructure(); 265 private final StringBuilder characterBuffer = new StringBuilder(); 266 private final LinkedList<ICalComponent> componentStack = new LinkedList<ICalComponent>(); 267 268 private Element propertyElement, parent; 269 private QName paramName; 270 private ICalComponent curComponent; 271 private ICalParameters parameters; 272 273 @Override 274 public void characters(char[] buffer, int start, int length) throws SAXException { 275 characterBuffer.append(buffer, start, length); 276 } 277 278 @Override 279 public void startElement(String namespace, String localName, String qName, Attributes attributes) throws SAXException { 280 QName qname = new QName(namespace, localName); 281 String textContent = emptyCharacterBuffer(); 282 283 if (structure.isEmpty()) { 284 //<icalendar> 285 if (ICALENDAR.equals(qname)) { 286 structure.push(ElementType.icalendar); 287 } 288 return; 289 } 290 291 ElementType parentType = structure.peek(); 292 ElementType typeToPush = null; 293 if (parentType != null) { 294 switch (parentType) { 295 296 case icalendar: 297 //<vcalendar> 298 if (VCALENDAR.equals(qname)) { 299 ICalComponentScribe<? extends ICalComponent> scribe = index.getComponentScribe(localName, ICalVersion.V2_0); 300 ICalComponent component = scribe.emptyInstance(); 301 302 curComponent = component; 303 readICal = (ICalendar) component; 304 typeToPush = ElementType.component; 305 } 306 break; 307 308 case component: 309 if (PROPERTIES.equals(qname)) { 310 //<properties> 311 typeToPush = ElementType.properties; 312 } else if (COMPONENTS.equals(qname)) { 313 //<components> 314 componentStack.add(curComponent); 315 curComponent = null; 316 317 typeToPush = ElementType.components; 318 } 319 break; 320 321 case components: 322 //start component element 323 if (XCAL_NS.equals(namespace)) { 324 ICalComponentScribe<? extends ICalComponent> scribe = index.getComponentScribe(localName, ICalVersion.V2_0); 325 curComponent = scribe.emptyInstance(); 326 327 ICalComponent parent = componentStack.getLast(); 328 parent.addComponent(curComponent); 329 330 typeToPush = ElementType.component; 331 } 332 break; 333 334 case properties: 335 //start property element 336 propertyElement = createElement(namespace, localName, attributes); 337 parameters = new ICalParameters(); 338 parent = propertyElement; 339 typeToPush = ElementType.property; 340 break; 341 342 case property: 343 //<parameters> 344 if (PARAMETERS.equals(qname)) { 345 typeToPush = ElementType.parameters; 346 } 347 break; 348 349 case parameters: 350 //inside of <parameters> 351 if (XCAL_NS.equals(namespace)) { 352 paramName = qname; 353 typeToPush = ElementType.parameter; 354 } 355 break; 356 357 case parameter: 358 //inside of a parameter element 359 if (XCAL_NS.equals(namespace)) { 360 typeToPush = ElementType.parameterValue; 361 } 362 break; 363 case parameterValue: 364 //should never have child elements 365 break; 366 } 367 } 368 369 //append element to property element 370 if (propertyElement != null && typeToPush != ElementType.property && typeToPush != ElementType.parameters && !structure.isUnderParameters()) { 371 if (textContent.length() > 0) { 372 parent.appendChild(DOC.createTextNode(textContent)); 373 } 374 375 Element element = createElement(namespace, localName, attributes); 376 parent.appendChild(element); 377 parent = element; 378 } 379 380 structure.push(typeToPush); 381 } 382 383 @Override 384 public void endElement(String namespace, String localName, String qName) throws SAXException { 385 String textContent = emptyCharacterBuffer(); 386 387 if (structure.isEmpty()) { 388 //no <icalendar> elements were read yet 389 return; 390 } 391 392 ElementType type = structure.pop(); 393 if (type == null && (propertyElement == null || structure.isUnderParameters())) { 394 //it's a non-xCal element 395 return; 396 } 397 398 if (type != null) { 399 switch (type) { 400 case parameterValue: 401 parameters.put(paramName.getLocalPart(), textContent); 402 break; 403 404 case parameter: 405 //do nothing 406 break; 407 408 case parameters: 409 //do nothing 410 break; 411 412 case property: 413 context.getWarnings().clear(); 414 415 propertyElement.appendChild(DOC.createTextNode(textContent)); 416 417 //unmarshal property and add to parent component 418 QName propertyQName = new QName(propertyElement.getNamespaceURI(), propertyElement.getLocalName()); 419 String propertyName = localName; 420 ICalPropertyScribe<? extends ICalProperty> scribe = index.getPropertyScribe(propertyQName); 421 try { 422 ICalProperty property = scribe.parseXml(propertyElement, parameters, context); 423 if (property instanceof Version && curComponent instanceof ICalendar) { 424 Version versionProp = (Version) property; 425 ICalVersion version = versionProp.toICalVersion(); 426 if (version != null) { 427 ICalendar ical = (ICalendar) curComponent; 428 ical.setVersion(version); 429 context.setVersion(version); 430 431 propertyElement = null; 432 break; 433 } 434 } 435 436 curComponent.addProperty(property); 437 for (Warning warning : context.getWarnings()) { 438 warnings.add(null, propertyName, warning); 439 } 440 } catch (SkipMeException e) { 441 warnings.add(null, propertyName, 22, e.getMessage()); 442 } catch (CannotParseException e) { 443 String xml = XmlUtils.toString(propertyElement); 444 warnings.add(null, propertyName, 33, xml, e.getMessage()); 445 446 scribe = index.getPropertyScribe(Xml.class); 447 ICalProperty property = scribe.parseXml(propertyElement, parameters, context); 448 curComponent.addProperty(property); 449 } 450 451 propertyElement = null; 452 break; 453 454 case component: 455 curComponent = null; 456 457 //</vcalendar> 458 if (VCALENDAR.getNamespaceURI().equals(namespace) && VCALENDAR.getLocalPart().equals(localName)) { 459 //wait for readNext() to be called again 460 try { 461 readerBlock.put(lock); 462 threadBlock.take(); 463 } catch (InterruptedException e) { 464 throw new SAXException(e); 465 } 466 return; 467 } 468 break; 469 470 case properties: 471 break; 472 473 case components: 474 curComponent = componentStack.removeLast(); 475 break; 476 477 case icalendar: 478 break; 479 } 480 } 481 482 //append element to property element 483 if (propertyElement != null && type != ElementType.property && type != ElementType.parameters && !structure.isUnderParameters()) { 484 if (textContent.length() > 0) { 485 parent.appendChild(DOC.createTextNode(textContent)); 486 } 487 parent = (Element) parent.getParentNode(); 488 } 489 } 490 491 private String emptyCharacterBuffer() { 492 String textContent = characterBuffer.toString(); 493 characterBuffer.setLength(0); 494 return textContent; 495 } 496 497 private Element createElement(String namespace, String localName, Attributes attributes) { 498 Element element = DOC.createElementNS(namespace, localName); 499 applyAttributesTo(element, attributes); 500 return element; 501 } 502 503 private void applyAttributesTo(Element element, Attributes attributes) { 504 for (int i = 0; i < attributes.getLength(); i++) { 505 String qname = attributes.getQName(i); 506 if (qname.startsWith("xmlns:")) { 507 continue; 508 } 509 510 String name = attributes.getLocalName(i); 511 String value = attributes.getValue(i); 512 element.setAttribute(name, value); 513 } 514 } 515 } 516 517 private enum ElementType { 518 //a value is missing for "vcalendar" because it is treated as a "component" 519 //enum values are lower-case so they won't get confused with the "XCalQNames" variable names 520 icalendar, components, properties, component, property, parameters, parameter, parameterValue; 521 } 522 523 /** 524 * <p> 525 * Keeps track of the structure of an xCal XML document. 526 * </p> 527 * 528 * <p> 529 * Note that this class is here because you can't just do QName comparisons 530 * on a one-by-one basis. The location of an XML element within the XML 531 * document is important too. It's possible for two elements to have the 532 * same QName, but be treated differently depending on their location (e.g. 533 * the {@code <duration>} property has a {@code <duration>} data type) 534 * </p> 535 */ 536 private static class XCalStructure { 537 private final List<ElementType> stack = new ArrayList<ElementType>(); 538 539 /** 540 * Pops the top element type off the stack. 541 * @return the element type or null if the stack is empty 542 */ 543 public ElementType pop() { 544 return isEmpty() ? null : stack.remove(stack.size() - 1); 545 } 546 547 /** 548 * Looks at the top element type. 549 * @return the top element type or null if the stack is empty 550 */ 551 public ElementType peek() { 552 return isEmpty() ? null : stack.get(stack.size() - 1); 553 } 554 555 /** 556 * Adds an element type to the stack. 557 * @param type the type to add or null if the XML element is not an xCal 558 * element 559 */ 560 public void push(ElementType type) { 561 stack.add(type); 562 } 563 564 /** 565 * Determines if the leaf node is under a {@code <parameters>} element. 566 * @return true if it is, false if not 567 */ 568 public boolean isUnderParameters() { 569 //get the first non-null type 570 ElementType nonNull = null; 571 for (int i = stack.size() - 1; i >= 0; i--) { 572 ElementType type = stack.get(i); 573 if (type != null) { 574 nonNull = type; 575 break; 576 } 577 } 578 579 //@formatter:off 580 return 581 nonNull == ElementType.parameters || 582 nonNull == ElementType.parameter || 583 nonNull == ElementType.parameterValue; 584 //@formatter:on 585 } 586 587 /** 588 * Determines if the stack is empty 589 * @return true if the stack is empty, false if not 590 */ 591 public boolean isEmpty() { 592 return stack.isEmpty(); 593 } 594 } 595 596 /** 597 * An implementation of {@link ErrorListener} that doesn't do anything. 598 */ 599 private static class NoOpErrorListener implements ErrorListener { 600 public void error(TransformerException e) { 601 //do nothing 602 } 603 604 public void fatalError(TransformerException e) { 605 //do nothing 606 } 607 608 public void warning(TransformerException e) { 609 //do nothing 610 } 611 } 612 613 /** 614 * Closes the underlying input stream. 615 */ 616 public void close() throws IOException { 617 if (thread.isAlive()) { 618 thread.closed = true; 619 thread.interrupt(); 620 } 621 622 if (stream != null) { 623 stream.close(); 624 } 625 } 626}