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.IOException; 011import java.io.InputStream; 012import java.io.Reader; 013import java.io.StringReader; 014import java.nio.file.Files; 015import java.nio.file.Path; 016import java.util.ArrayList; 017import java.util.List; 018import java.util.concurrent.ArrayBlockingQueue; 019import java.util.concurrent.BlockingQueue; 020import java.util.stream.IntStream; 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-2026, 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 * Path file = Paths.get("vcards.xml"); 093 * try (XCardReader reader = new XCardReader(file)) { 094 * VCard vcard; 095 * while ((vcard = reader.readNext()) != null) { 096 * //... 097 * } 098 * } 099 * </pre> 100 * 101 * @author Michael Angstadt 102 * @see <a href="http://tools.ietf.org/html/rfc6351">RFC 6351</a> 103 */ 104public class XCardReader extends StreamReader { 105 private final VCardVersion version = VCardVersion.V4_0; 106 private final String NS = version.getXmlNamespace(); 107 108 private final Source source; 109 private final Closeable stream; 110 111 private volatile VCard readVCard; 112 private volatile TransformerException thrown; 113 114 private final ReadThread thread = new ReadThread(); 115 private final Object lock = new Object(); 116 private final BlockingQueue<Object> readerBlock = new ArrayBlockingQueue<>(1); 117 private final BlockingQueue<Object> threadBlock = new ArrayBlockingQueue<>(1); 118 119 /** 120 * @param xml the XML to read from 121 */ 122 public XCardReader(String xml) { 123 this(new StringReader(xml)); 124 } 125 126 /** 127 * @param in the input stream to read from 128 */ 129 public XCardReader(InputStream in) { 130 source = new StreamSource(in); 131 stream = in; 132 } 133 134 /** 135 * @param file the file to read from 136 * @throws IOException if there is a problem opening the file 137 */ 138 public XCardReader(Path file) throws IOException { 139 this(new BufferedInputStream(Files.newInputStream(file))); 140 } 141 142 /** 143 * @param reader the reader to read from 144 */ 145 public XCardReader(Reader reader) { 146 source = new StreamSource(reader); 147 stream = reader; 148 } 149 150 /** 151 * @param node the DOM node to read from 152 */ 153 public XCardReader(Node node) { 154 source = new DOMSource(node); 155 stream = null; 156 } 157 158 @Override 159 protected VCard _readNext() throws IOException { 160 readVCard = null; 161 thrown = null; 162 context.setVersion(version); 163 164 if (!thread.started) { 165 thread.start(); 166 } else { 167 if (thread.finished || thread.closed) { 168 return null; 169 } 170 171 try { 172 threadBlock.put(lock); 173 } catch (InterruptedException e) { 174 Thread.currentThread().interrupt(); 175 return null; 176 } 177 } 178 179 //wait until thread reads xCard 180 try { 181 readerBlock.take(); 182 } catch (InterruptedException e) { 183 Thread.currentThread().interrupt(); 184 return null; 185 } 186 187 if (thrown != null) { 188 throw new IOException(thrown); 189 } 190 191 return readVCard; 192 } 193 194 private class ReadThread extends Thread { 195 private final SAXResult result; 196 private final Transformer transformer; 197 private volatile boolean finished = false; 198 private volatile boolean started = false; 199 private volatile boolean 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 Thread.currentThread().interrupt(); 237 //ignore 238 } 239 } 240 } 241 } 242 243 private class ContentHandlerImpl extends DefaultHandler { 244 private final Document DOC = XmlUtils.createDocument(); 245 private final XCardStructure structure = new XCardStructure(); 246 private final ClearableStringBuilder characterBuffer = new ClearableStringBuilder(); 247 248 private String group; 249 private Element propertyElement; 250 private Element parent; 251 private QName paramName; 252 private VCardParameters parameters; 253 254 @Override 255 public void characters(char[] buffer, int start, int length) throws SAXException { 256 /* 257 * Ignore all text nodes that are outside of a property element. All 258 * valid text nodes will be inside of property elements (parameter 259 * values and property values) 260 */ 261 if (propertyElement == null) { 262 return; 263 } 264 265 characterBuffer.append(buffer, start, length); 266 } 267 268 @Override 269 public void startElement(String namespace, String localName, String qName, Attributes attributes) throws SAXException { 270 QName qname = new QName(namespace, localName); 271 String textContent = characterBuffer.getAndClear(); 272 273 //ignore all XML until a <vcards> elements is read 274 if (!isInsideVCardsElement(qname)) { 275 return; 276 } 277 278 ElementType parentType = structure.peek(); 279 ElementType typeToPush = (parentType == null) ? null : processStartElement(parentType, qname, attributes); 280 281 if (shouldElementBeAppendedToPropertyElement(typeToPush)) { 282 appendNonEmptyTextContentToParent(textContent); 283 284 Element element = createElement(namespace, localName, attributes); 285 parent.appendChild(element); 286 parent = element; 287 } 288 289 structure.push(typeToPush); 290 } 291 292 private boolean isInsideVCardsElement(QName qname) { 293 if (!structure.isEmpty()) { 294 return true; 295 } 296 297 if (VCARDS.equals(qname)) { 298 structure.push(ElementType.vcards); 299 } 300 301 return false; 302 } 303 304 private ElementType processStartElement(ElementType parentType, QName qname, Attributes attributes) { 305 switch (parentType) { 306 case vcards: 307 //<vcard> 308 if (VCARD.equals(qname)) { 309 readVCard = new VCard(); 310 readVCard.setVersion(version); 311 return ElementType.vcard; 312 } 313 break; 314 315 case vcard: 316 //<group> 317 if (GROUP.equals(qname)) { 318 group = attributes.getValue("name"); 319 return ElementType.group; 320 } else { 321 propertyElement = createElement(qname.getNamespaceURI(), qname.getLocalPart(), attributes); 322 parameters = new VCardParameters(); 323 parent = propertyElement; 324 return ElementType.property; 325 } 326 327 case group: 328 propertyElement = createElement(qname.getNamespaceURI(), qname.getLocalPart(), attributes); 329 parameters = new VCardParameters(); 330 parent = propertyElement; 331 return ElementType.property; 332 333 case property: 334 //<parameters> 335 if (PARAMETERS.equals(qname)) { 336 return ElementType.parameters; 337 } 338 break; 339 340 case parameters: 341 //inside of <parameters> 342 if (NS.equals(qname.getNamespaceURI())) { 343 paramName = qname; 344 return ElementType.parameter; 345 } 346 break; 347 348 case parameter: 349 if (NS.equals(qname.getNamespaceURI())) { 350 return ElementType.parameterValue; 351 } 352 break; 353 354 case parameterValue: 355 //should never have child elements 356 break; 357 } 358 359 return null; 360 } 361 362 @Override 363 public void endElement(String namespace, String localName, String qName) throws SAXException { 364 String textContent = characterBuffer.getAndClear(); 365 366 //ignore all XML until a <vcards> elements is read 367 boolean isInsideVCardsElement = !structure.isEmpty(); 368 if (!isInsideVCardsElement) { 369 return; 370 } 371 372 ElementType type = structure.pop(); 373 boolean isNotAnXCardElement = (type == null && (propertyElement == null || structure.isUnderParameters())); 374 if (isNotAnXCardElement) { 375 return; 376 } 377 378 if (type != null) { 379 processEndElement(type, localName, textContent); 380 } 381 382 if (shouldElementBeAppendedToPropertyElement(type)) { 383 appendNonEmptyTextContentToParent(textContent); 384 parent = (Element) parent.getParentNode(); 385 } 386 } 387 388 private boolean shouldElementBeAppendedToPropertyElement(ElementType type) { 389 return propertyElement != null && type != ElementType.property && type != ElementType.parameters && !structure.isUnderParameters(); 390 } 391 392 private void appendNonEmptyTextContentToParent(String text) { 393 if (!text.isEmpty()) { 394 parent.appendChild(DOC.createTextNode(text)); 395 } 396 } 397 398 private void processEndElement(ElementType type, String localName, String textContent) throws SAXException { 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 propertyElement.appendChild(DOC.createTextNode(textContent)); 414 415 String propertyName = localName; 416 VCardProperty property; 417 QName propertyQName = new QName(propertyElement.getNamespaceURI(), propertyElement.getLocalName()); 418 VCardPropertyScribe<? extends VCardProperty> scribe = index.getPropertyScribe(propertyQName); 419 420 context.getWarnings().clear(); 421 context.setPropertyName(propertyName); 422 try { 423 property = scribe.parseXml(propertyElement, parameters, context); 424 property.setGroup(group); 425 readVCard.addProperty(property); 426 warnings.addAll(context.getWarnings()); 427 } catch (SkipMeException e) { 428 //@formatter:off 429 warnings.add(new ParseWarning.Builder(context) 430 .message(22, e.getMessage()) 431 .build() 432 ); 433 //@formatter:on 434 } catch (CannotParseException e) { 435 //@formatter:off 436 warnings.add(new ParseWarning.Builder(context) 437 .message(e) 438 .build() 439 ); 440 //@formatter:on 441 442 scribe = index.getPropertyScribe(Xml.class); 443 property = scribe.parseXml(propertyElement, parameters, context); 444 property.setGroup(group); 445 readVCard.addProperty(property); 446 } catch (EmbeddedVCardException e) { 447 //@formatter:off 448 warnings.add(new ParseWarning.Builder(context) 449 .message(34) 450 .build() 451 ); 452 //@formatter:on 453 } 454 455 propertyElement = null; 456 break; 457 458 case group: 459 group = null; 460 break; 461 462 case vcard: 463 //wait for readNext() to be called again 464 try { 465 readerBlock.put(lock); 466 threadBlock.take(); 467 } catch (InterruptedException e) { 468 Thread.currentThread().interrupt(); 469 throw new SAXException(e); 470 } 471 break; 472 473 case vcards: 474 //do nothing 475 break; 476 } 477 } 478 479 private Element createElement(String namespace, String localName, Attributes attributes) { 480 Element element = DOC.createElementNS(namespace, localName); 481 applyAttributes(element, attributes); 482 return element; 483 } 484 485 private void applyAttributes(Element element, Attributes attributes) { 486 //@formatter:off 487 IntStream.range(0, attributes.getLength()) 488 .filter(i -> !attributes.getQName(i).startsWith("xmlns:")) 489 .forEach(i -> { 490 String name = attributes.getLocalName(i); 491 String value = attributes.getValue(i); 492 element.setAttribute(name, value); 493 }); 494 //@formatter:on 495 } 496 } 497 498 private enum ElementType { 499 //enum values are lower-case so they won't get confused with the "XCardQNames" variable names 500 vcards, vcard, group, property, parameters, parameter, parameterValue 501 } 502 503 /** 504 * <p> 505 * Keeps track of the structure of an xCard XML document. 506 * </p> 507 * 508 * <p> 509 * Note that this class is here because you can't just do QName comparisons 510 * on a one-by-one basis. The location of an XML element within the XML 511 * document is important too. It's possible for two elements to have the 512 * same QName, but be treated differently depending on their location (e.g. 513 * a parameter named "parameters") 514 * </p> 515 */ 516 private static class XCardStructure { 517 private final List<ElementType> stack = new ArrayList<>(); 518 519 /** 520 * Pops the top element type off the stack. 521 * @return the element type or null if the stack is empty 522 */ 523 public ElementType pop() { 524 return isEmpty() ? null : stack.remove(stack.size() - 1); 525 } 526 527 /** 528 * Looks at the top element type. 529 * @return the top element type or null if the stack is empty 530 */ 531 public ElementType peek() { 532 return isEmpty() ? null : stack.get(stack.size() - 1); 533 } 534 535 /** 536 * Adds an element type to the stack. 537 * @param type the type to add or null if the XML element is not an 538 * xCard element 539 */ 540 public void push(ElementType type) { 541 stack.add(type); 542 } 543 544 /** 545 * Determines if the leaf node is under a {@code <parameters>} element. 546 * @return true if it is, false if not 547 */ 548 public boolean isUnderParameters() { 549 //get the first non-null type 550 ElementType nonNull = null; 551 for (int i = stack.size() - 1; i >= 0; i--) { 552 ElementType type = stack.get(i); 553 if (type != null) { 554 nonNull = type; 555 break; 556 } 557 } 558 559 //@formatter:off 560 return 561 nonNull == ElementType.parameters || 562 nonNull == ElementType.parameter || 563 nonNull == ElementType.parameterValue; 564 //@formatter:on 565 } 566 567 /** 568 * Determines if the stack is empty 569 * @return true if the stack is empty, false if not 570 */ 571 public boolean isEmpty() { 572 return stack.isEmpty(); 573 } 574 } 575 576 /** 577 * An implementation of {@link ErrorListener} that doesn't do anything. 578 */ 579 private static class NoOpErrorListener implements ErrorListener { 580 public void error(TransformerException e) { 581 //do nothing 582 } 583 584 public void fatalError(TransformerException e) { 585 //do nothing 586 } 587 588 public void warning(TransformerException e) { 589 //do nothing 590 } 591 } 592 593 /** 594 * Closes the underlying input stream. 595 */ 596 public void close() throws IOException { 597 if (thread.isAlive()) { 598 thread.closed = true; 599 thread.interrupt(); 600 } 601 602 if (stream != null) { 603 stream.close(); 604 } 605 } 606}