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 vCards) in a streaming fashion. 091 * </p> 092 * <p> 093 * <b>Example:</b> 094 * 095 * <pre class="brush:java"> 096 * File file = new File("xcals.xml"); 097 * List<ICalendar> icals = new ArrayList<ICalendar>(); 098 * XCalReader xcalReader = new XCalReader(file); 099 * ICalendar ical; 100 * while ((ical = xcalReader.readNext()) != null) { 101 * icals.add(ical); 102 * } 103 * </pre> 104 * 105 * </p> 106 * 107 * <p> 108 * <b>Getting timezone information:</b> 109 * 110 * <pre class="brush:java"> 111 * XCalReader reader = ... 112 * ICalendar ical = reader.readNext(); 113 * TimezoneInfo tzinfo = reader.getTimezoneInfo(); 114 * 115 * //get the VTIMEZONE components that were parsed 116 * //the VTIMEZONE components will NOT be in the ICalendar object 117 * Collection<VTimezone> vtimezones = tzinfo.getComponents(); 118 * 119 * //get the timezone that a property was originally formatted in 120 * DateStart dtstart = ical.getEvents().get(0).getDateStart(); 121 * TimeZone tz = tzinfo.getTimeZone(dtstart); 122 * </pre> 123 * 124 * </p> 125 * @author Michael Angstadt 126 * @see <a href="http://tools.ietf.org/html/rfc6321">RFC 6321</a> 127 */ 128public class XCalReader extends StreamReader { 129 private final Source source; 130 private final Closeable stream; 131 132 private volatile ICalendar readICal; 133 private volatile TransformerException thrown; 134 135 private final ReadThread thread = new ReadThread(); 136 private final Object lock = new Object(); 137 private final BlockingQueue<Object> readerBlock = new ArrayBlockingQueue<Object>(1); 138 private final BlockingQueue<Object> threadBlock = new ArrayBlockingQueue<Object>(1); 139 140 /** 141 * Creates an xCal reader. 142 * @param str the string to read the xCals from 143 */ 144 public XCalReader(String str) { 145 this(new StringReader(str)); 146 } 147 148 /** 149 * Creates an xCal reader. 150 * @param in the input stream to read the xCals from 151 */ 152 public XCalReader(InputStream in) { 153 source = new StreamSource(in); 154 stream = in; 155 } 156 157 /** 158 * Creates an xCal reader. 159 * @param file the file to read the xCals from 160 * @throws FileNotFoundException if the file doesn't exist 161 */ 162 public XCalReader(File file) throws FileNotFoundException { 163 this(new FileInputStream(file)); 164 } 165 166 /** 167 * Creates an xCal reader. 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 * Creates an xCal reader. 177 * @param node the DOM node to read from 178 */ 179 public XCalReader(Node node) { 180 source = new DOMSource(node); 181 stream = null; 182 } 183 184 @Override 185 protected ICalendar _readNext() throws IOException { 186 readICal = null; 187 warnings.clear(); 188 context = new ParseContext(); 189 tzinfo = new TimezoneInfo(); 190 thrown = null; 191 192 if (!thread.started) { 193 thread.start(); 194 } else { 195 if (thread.finished || thread.closed) { 196 return null; 197 } 198 199 try { 200 threadBlock.put(lock); 201 } catch (InterruptedException e) { 202 return null; 203 } 204 } 205 206 //wait until thread reads xCard 207 try { 208 readerBlock.take(); 209 } catch (InterruptedException e) { 210 return null; 211 } 212 213 if (thrown != null) { 214 throw new IOException(thrown); 215 } 216 217 return readICal; 218 } 219 220 private class ReadThread extends Thread { 221 private final SAXResult result; 222 private final Transformer transformer; 223 private volatile boolean finished = false, started = false, closed = false; 224 225 public ReadThread() { 226 setName(getClass().getSimpleName()); 227 228 //create the transformer 229 try { 230 transformer = TransformerFactory.newInstance().newTransformer(); 231 } catch (TransformerConfigurationException e) { 232 //no complex configurations 233 throw new RuntimeException(e); 234 } 235 236 //prevent error messages from being printed to stderr 237 transformer.setErrorListener(new ErrorListener() { 238 public void error(TransformerException e) { 239 //empty 240 } 241 242 public void fatalError(TransformerException e) { 243 //empty 244 } 245 246 public void warning(TransformerException e) { 247 //empty 248 } 249 }); 250 251 result = new SAXResult(new ContentHandlerImpl()); 252 } 253 254 @Override 255 public void run() { 256 started = true; 257 258 try { 259 transformer.transform(source, result); 260 } catch (TransformerException e) { 261 if (!thread.closed) { 262 thrown = e; 263 } 264 } finally { 265 finished = true; 266 try { 267 readerBlock.put(lock); 268 } catch (InterruptedException e) { 269 //ignore 270 } 271 } 272 } 273 } 274 275 private class ContentHandlerImpl extends DefaultHandler { 276 private final Document DOC = XmlUtils.createDocument(); 277 private final XCalStructure structure = new XCalStructure(); 278 private final StringBuilder characterBuffer = new StringBuilder(); 279 private final LinkedList<ICalComponent> componentStack = new LinkedList<ICalComponent>(); 280 281 private Element propertyElement, parent; 282 private QName paramName; 283 private ICalComponent curComponent; 284 private ICalParameters parameters; 285 286 @Override 287 public void characters(char[] buffer, int start, int length) throws SAXException { 288 characterBuffer.append(buffer, start, length); 289 } 290 291 @Override 292 public void startElement(String namespace, String localName, String qName, Attributes attributes) throws SAXException { 293 QName qname = new QName(namespace, localName); 294 String textContent = characterBuffer.toString(); 295 characterBuffer.setLength(0); 296 297 if (structure.isEmpty()) { 298 //<icalendar> 299 if (ICALENDAR.equals(qname)) { 300 structure.push(ElementType.icalendar); 301 } 302 return; 303 } 304 305 ElementType parentType = structure.peek(); 306 ElementType typeToPush = null; 307 //System.out.println(structure.stack + " current: " + localName); 308 if (parentType != null) { 309 switch (parentType) { 310 311 case icalendar: 312 //<vcalendar> 313 if (VCALENDAR.equals(qname)) { 314 ICalComponentScribe<? extends ICalComponent> scribe = index.getComponentScribe(localName, ICalVersion.V2_0); 315 ICalComponent component = scribe.emptyInstance(); 316 317 curComponent = component; 318 readICal = (ICalendar) component; 319 typeToPush = ElementType.component; 320 } 321 break; 322 323 case component: 324 if (PROPERTIES.equals(qname)) { 325 //<properties> 326 typeToPush = ElementType.properties; 327 } else if (COMPONENTS.equals(qname)) { 328 //<components> 329 componentStack.add(curComponent); 330 curComponent = null; 331 332 typeToPush = ElementType.components; 333 } 334 break; 335 336 case components: 337 //start component element 338 if (XCAL_NS.equals(namespace)) { 339 ICalComponentScribe<? extends ICalComponent> scribe = index.getComponentScribe(localName, ICalVersion.V2_0); 340 curComponent = scribe.emptyInstance(); 341 342 ICalComponent parent = componentStack.getLast(); 343 parent.addComponent(curComponent); 344 345 typeToPush = ElementType.component; 346 } 347 break; 348 349 case properties: 350 //start property element 351 propertyElement = createElement(namespace, localName, attributes); 352 parameters = new ICalParameters(); 353 parent = propertyElement; 354 typeToPush = ElementType.property; 355 break; 356 357 case property: 358 //<parameters> 359 if (PARAMETERS.equals(qname)) { 360 typeToPush = ElementType.parameters; 361 } 362 break; 363 364 case parameters: 365 //inside of <parameters> 366 if (XCAL_NS.equals(namespace)) { 367 paramName = qname; 368 typeToPush = ElementType.parameter; 369 } 370 break; 371 372 case parameter: 373 //inside of a parameter element 374 if (XCAL_NS.equals(namespace)) { 375 typeToPush = ElementType.parameterValue; 376 } 377 break; 378 case parameterValue: 379 //should never have child elements 380 break; 381 } 382 } 383 384 //append element to property element 385 if (propertyElement != null && typeToPush != ElementType.property && typeToPush != ElementType.parameters && !structure.isUnderParameters()) { 386 if (textContent.length() > 0) { 387 parent.appendChild(DOC.createTextNode(textContent)); 388 } 389 390 Element element = createElement(namespace, localName, attributes); 391 parent.appendChild(element); 392 parent = element; 393 } 394 395 structure.push(typeToPush); 396 } 397 398 @Override 399 public void endElement(String namespace, String localName, String qName) throws SAXException { 400 String textContent = characterBuffer.toString(); 401 characterBuffer.setLength(0); 402 403 if (structure.isEmpty()) { 404 //no <icalendar> elements were read yet 405 return; 406 } 407 408 ElementType type = structure.pop(); 409 if (type == null && (propertyElement == null || structure.isUnderParameters())) { 410 //it's a non-xCal element 411 return; 412 } 413 414 //System.out.println(structure.stack + " ending: " + localName); 415 if (type != null) { 416 switch (type) { 417 case parameterValue: 418 parameters.put(paramName.getLocalPart(), textContent); 419 break; 420 421 case parameter: 422 //do nothing 423 break; 424 425 case parameters: 426 //do nothing 427 break; 428 429 case property: 430 context.getWarnings().clear(); 431 432 propertyElement.appendChild(DOC.createTextNode(textContent)); 433 434 //unmarshal property and add to parent component 435 QName propertyQName = new QName(propertyElement.getNamespaceURI(), propertyElement.getLocalName()); 436 String propertyName = localName; 437 ICalPropertyScribe<? extends ICalProperty> scribe = index.getPropertyScribe(propertyQName); 438 try { 439 ICalProperty property = scribe.parseXml(propertyElement, parameters, context); 440 if (property instanceof Version && curComponent instanceof ICalendar) { 441 Version versionProp = (Version) property; 442 ICalVersion version = versionProp.toICalVersion(); 443 if (version != null) { 444 ICalendar ical = (ICalendar) curComponent; 445 ical.setVersion(version); 446 context.setVersion(version); 447 448 propertyElement = null; 449 break; 450 } 451 } 452 453 curComponent.addProperty(property); 454 for (Warning warning : context.getWarnings()) { 455 warnings.add(null, propertyName, warning); 456 } 457 } catch (SkipMeException e) { 458 warnings.add(null, propertyName, 22, e.getMessage()); 459 } catch (CannotParseException e) { 460 String xml = XmlUtils.toString(propertyElement); 461 warnings.add(null, propertyName, 33, xml, e.getMessage()); 462 463 scribe = index.getPropertyScribe(Xml.class); 464 ICalProperty property = scribe.parseXml(propertyElement, parameters, context); 465 curComponent.addProperty(property); 466 } 467 468 propertyElement = null; 469 break; 470 471 case component: 472 curComponent = null; 473 474 //</vcalendar> 475 if (VCALENDAR.getNamespaceURI().equals(namespace) && VCALENDAR.getLocalPart().equals(localName)) { 476 //wait for readNext() to be called again 477 try { 478 readerBlock.put(lock); 479 threadBlock.take(); 480 } catch (InterruptedException e) { 481 throw new SAXException(e); 482 } 483 return; 484 } 485 break; 486 487 case properties: 488 break; 489 490 case components: 491 curComponent = componentStack.removeLast(); 492 break; 493 494 case icalendar: 495 break; 496 } 497 } 498 499 //append element to property element 500 if (propertyElement != null && type != ElementType.property && type != ElementType.parameters && !structure.isUnderParameters()) { 501 if (textContent.length() > 0) { 502 parent.appendChild(DOC.createTextNode(textContent)); 503 } 504 parent = (Element) parent.getParentNode(); 505 } 506 } 507 508 private Element createElement(String namespace, String localName, Attributes attributes) { 509 Element element = DOC.createElementNS(namespace, localName); 510 511 //copy the attributes 512 for (int i = 0; i < attributes.getLength(); i++) { 513 String qname = attributes.getQName(i); 514 if (qname.startsWith("xmlns:")) { 515 continue; 516 } 517 518 String name = attributes.getLocalName(i); 519 String value = attributes.getValue(i); 520 element.setAttribute(name, value); 521 } 522 523 return element; 524 } 525 } 526 527 private enum ElementType { 528 //a value is missing for "vcalendar" because it is treated as a "component" 529 //enum values are lower-case so they won't get confused with the "XCalQNames" variable names 530 icalendar, components, properties, component, property, parameters, parameter, parameterValue; 531 } 532 533 /** 534 * <p> 535 * Keeps track of the structure of an xCal XML document. 536 * </p> 537 * 538 * <p> 539 * Note that this class is here because you can't just do QName comparisons 540 * on a one-by-one basis. The location of an XML element within the XML 541 * document is important too. It's possible for two elements to have the 542 * same QName, but be treated differently depending on their location (e.g. 543 * the {@code <duration>} property has a {@code <duration>} data type) 544 * </p> 545 */ 546 private static class XCalStructure { 547 private final List<ElementType> stack = new ArrayList<ElementType>(); 548 549 /** 550 * Pops the top element type off the stack. 551 * @return the element type or null if the stack is empty 552 */ 553 public ElementType pop() { 554 return stack.isEmpty() ? null : stack.remove(stack.size() - 1); 555 } 556 557 /** 558 * Looks at the top element type. 559 * @return the top element type or null if the stack is empty 560 */ 561 public ElementType peek() { 562 return stack.isEmpty() ? null : stack.get(stack.size() - 1); 563 } 564 565 /** 566 * Adds an element type to the stack. 567 * @param type the type to add or null if the XML element is not an xCal 568 * element 569 */ 570 public void push(ElementType type) { 571 stack.add(type); 572 } 573 574 /** 575 * Determines if the leaf node is under a {@code <parameters>} element. 576 * @return true if it is, false if not 577 */ 578 public boolean isUnderParameters() { 579 //get the first non-null type 580 ElementType nonNull = null; 581 for (int i = stack.size() - 1; i >= 0; i--) { 582 ElementType type = stack.get(i); 583 if (type != null) { 584 nonNull = type; 585 break; 586 } 587 } 588 589 return nonNull == ElementType.parameters || nonNull == ElementType.parameter || nonNull == ElementType.parameterValue; 590 } 591 592 /** 593 * Determines if the stack is empty 594 * @return true if the stack is empty, false if not 595 */ 596 public boolean isEmpty() { 597 return stack.isEmpty(); 598 } 599 } 600 601 /** 602 * Closes the underlying input stream. 603 */ 604 public void close() throws IOException { 605 if (thread.isAlive()) { 606 thread.closed = true; 607 thread.interrupt(); 608 } 609 610 if (stream != null) { 611 stream.close(); 612 } 613 } 614}