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