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