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