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.IOException; 009import java.io.OutputStream; 010import java.io.OutputStreamWriter; 011import java.io.Writer; 012import java.nio.charset.StandardCharsets; 013import java.nio.file.Files; 014import java.nio.file.Path; 015import java.util.Collections; 016import java.util.List; 017import java.util.Map; 018 019import javax.xml.namespace.QName; 020import javax.xml.transform.Result; 021import javax.xml.transform.Transformer; 022import javax.xml.transform.TransformerConfigurationException; 023import javax.xml.transform.TransformerFactory; 024import javax.xml.transform.dom.DOMResult; 025import javax.xml.transform.sax.SAXTransformerFactory; 026import javax.xml.transform.sax.TransformerHandler; 027import javax.xml.transform.stream.StreamResult; 028 029import org.w3c.dom.Document; 030import org.w3c.dom.Element; 031import org.w3c.dom.NamedNodeMap; 032import org.w3c.dom.Node; 033import org.w3c.dom.NodeList; 034import org.w3c.dom.Text; 035import org.xml.sax.Attributes; 036import org.xml.sax.SAXException; 037import org.xml.sax.helpers.AttributesImpl; 038 039import ezvcard.VCard; 040import ezvcard.VCardDataType; 041import ezvcard.io.EmbeddedVCardException; 042import ezvcard.io.SkipMeException; 043import ezvcard.io.scribe.VCardPropertyScribe; 044import ezvcard.parameter.VCardParameters; 045import ezvcard.property.VCardProperty; 046import ezvcard.property.Xml; 047import ezvcard.util.ListMultimap; 048import ezvcard.util.XmlUtils; 049 050/* 051 Copyright (c) 2012-2026, Michael Angstadt 052 All rights reserved. 053 054 Redistribution and use in source and binary forms, with or without 055 modification, are permitted provided that the following conditions are met: 056 057 1. Redistributions of source code must retain the above copyright notice, this 058 list of conditions and the following disclaimer. 059 2. Redistributions in binary form must reproduce the above copyright notice, 060 this list of conditions and the following disclaimer in the documentation 061 and/or other materials provided with the distribution. 062 063 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 064 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 065 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 066 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 067 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 068 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 069 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 070 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 071 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 072 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 073 074 The views and conclusions contained in the software and documentation are those 075 of the authors and should not be interpreted as representing official policies, 076 either expressed or implied, of the FreeBSD Project. 077 */ 078 079/** 080 * <p> 081 * Writes xCards (XML-encoded vCards) in a streaming fashion. 082 * </p> 083 * <p> 084 * <b>Example:</b> 085 * </p> 086 * 087 * <pre class="brush:java"> 088 * VCard vcard1 = ... 089 * VCard vcard2 = ... 090 * Path file = Paths.get("vcards.xml"); 091 * try (XCardWriter writer = new XCardWriter(file)) { 092 * writer.write(vcard1); 093 * writer.write(vcard2); 094 * } 095 * </pre> 096 * @author Michael Angstadt 097 * @see <a href="http://tools.ietf.org/html/rfc6351">RFC 6351</a> 098 */ 099public class XCardWriter extends XCardWriterBase { 100 //How to use SAX to write XML: http://stackoverflow.com/q/4898590 101 102 private final Document DOC = XmlUtils.createDocument(); 103 104 private final Writer writer; 105 private final TransformerHandler handler; 106 private final boolean vcardsElementExists; 107 private boolean started = false; 108 109 /** 110 * @param out the output stream to write to (UTF-8 encoding will be used) 111 */ 112 public XCardWriter(OutputStream out) { 113 this(out, (Integer) null); 114 } 115 116 /** 117 * @param out the output stream to write to (UTF-8 encoding will be used) 118 * @param indent the number of indent spaces to use for pretty-printing or 119 * null to disable pretty-printing (disabled by default) 120 */ 121 public XCardWriter(OutputStream out, Integer indent) { 122 this(out, indent, null); 123 } 124 125 /** 126 * @param out the output stream to write to (UTF-8 encoding will be used) 127 * @param indent the number of indent spaces to use for pretty-printing or 128 * null to disable pretty-printing (disabled by default) 129 * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many 130 * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library 131 * like <a href= 132 * "http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22xalan%22%20AND%20a%3A%22xalan%22" 133 * >xalan</a> to your project) 134 */ 135 public XCardWriter(OutputStream out, Integer indent, String xmlVersion) { 136 this(out, new XCardOutputProperties(indent, xmlVersion)); 137 } 138 139 /** 140 * @param out the output stream to write to (UTF-8 encoding will be used) 141 * @param outputProperties properties to assign to the JAXP transformer (see 142 * {@link Transformer#setOutputProperty}) 143 */ 144 public XCardWriter(OutputStream out, Map<String, String> outputProperties) { 145 this(new OutputStreamWriter(out, StandardCharsets.UTF_8), outputProperties); 146 } 147 148 /** 149 * @param file the file to write to (UTF-8 encoding will be used) 150 * @throws IOException if there is a problem opening the file 151 */ 152 public XCardWriter(Path file) throws IOException { 153 this(file, (Integer) null); 154 } 155 156 /** 157 * @param file the file to write to (UTF-8 encoding will be used) 158 * @param indent the number of indent spaces to use for pretty-printing or 159 * null to disable pretty-printing (disabled by default) 160 * @throws IOException if there is a problem opening the file 161 */ 162 public XCardWriter(Path file, Integer indent) throws IOException { 163 this(file, indent, null); 164 } 165 166 /** 167 * @param file the file to write to (UTF-8 encoding will be used) 168 * @param indent the number of indent spaces to use for pretty-printing or 169 * null to disable pretty-printing (disabled by default) 170 * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many 171 * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library 172 * like <a href= 173 * "http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22xalan%22%20AND%20a%3A%22xalan%22" 174 * >xalan</a> to your project) 175 * @throws IOException if there is a problem opening the file 176 */ 177 public XCardWriter(Path file, Integer indent, String xmlVersion) throws IOException { 178 this(file, new XCardOutputProperties(indent, xmlVersion)); 179 } 180 181 /** 182 * @param file the file to write to (UTF-8 encoding will be used) 183 * @param outputProperties properties to assign to the JAXP transformer (see 184 * {@link Transformer#setOutputProperty}) 185 * @throws IOException if there is a problem opening the file 186 */ 187 public XCardWriter(Path file, Map<String, String> outputProperties) throws IOException { 188 this(Files.newBufferedWriter(file, StandardCharsets.UTF_8), outputProperties); 189 } 190 191 /** 192 * @param writer the writer to write to 193 */ 194 public XCardWriter(Writer writer) { 195 this(writer, (Integer) null); 196 } 197 198 /** 199 * @param writer the writer to write to 200 * @param indent the number of indent spaces to use for pretty-printing or 201 * null to disable pretty-printing (disabled by default) 202 */ 203 public XCardWriter(Writer writer, Integer indent) { 204 this(writer, indent, null); 205 } 206 207 /** 208 * @param writer the writer to write to 209 * @param indent the number of indent spaces to use for pretty-printing or 210 * null to disable pretty-printing (disabled by default) 211 * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many 212 * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library 213 * like <a href= 214 * "http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22xalan%22%20AND%20a%3A%22xalan%22" 215 * >xalan</a> to your project) 216 */ 217 public XCardWriter(Writer writer, Integer indent, String xmlVersion) { 218 this(writer, new XCardOutputProperties(indent, xmlVersion)); 219 } 220 221 /** 222 * @param writer the writer to write to 223 * @param outputProperties properties to assign to the JAXP transformer (see 224 * {@link Transformer#setOutputProperty}) 225 */ 226 public XCardWriter(Writer writer, Map<String, String> outputProperties) { 227 this(writer, null, outputProperties); 228 } 229 230 /** 231 * @param parent the DOM node to add child elements to 232 */ 233 public XCardWriter(Node parent) { 234 this(null, parent, Collections.<String, String> emptyMap()); 235 } 236 237 private XCardWriter(Writer writer, Node parent, Map<String, String> outputProperties) { 238 this.writer = writer; 239 240 if (parent instanceof Document) { 241 Node root = ((Document) parent).getDocumentElement(); 242 if (root != null) { 243 parent = root; 244 } 245 } 246 this.vcardsElementExists = isVCardsElement(parent); 247 248 try { 249 SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance(); 250 handler = factory.newTransformerHandler(); 251 } catch (TransformerConfigurationException e) { 252 throw new RuntimeException(e); 253 } 254 255 Transformer transformer = handler.getTransformer(); 256 257 /* 258 * Using Transformer#setOutputProperties(Properties) doesn't work for 259 * some reason for setting the number of indentation spaces. 260 */ 261 outputProperties.forEach(transformer::setOutputProperty); 262 263 Result result = (writer == null) ? new DOMResult(parent) : new StreamResult(writer); 264 handler.setResult(result); 265 } 266 267 private boolean isVCardsElement(Node node) { 268 if (node == null) { 269 return false; 270 } 271 272 if (!(node instanceof Element)) { 273 return false; 274 } 275 276 return XmlUtils.hasQName(node, VCARDS); 277 } 278 279 @Override 280 protected void _write(VCard vcard, List<VCardProperty> properties) throws IOException { 281 try { 282 if (!started) { 283 handler.startDocument(); 284 285 if (!vcardsElementExists) { 286 //don't output a <vcards> element if the parent is a <vcards> element 287 start(VCARDS); 288 } 289 290 started = true; 291 } 292 293 ListMultimap<String, VCardProperty> propertiesByGroup = new ListMultimap<>(); //group the types by group name (null = no group name) 294 properties.forEach(property -> propertiesByGroup.put(property.getGroup(), property)); 295 296 start(VCARD); 297 298 for (Map.Entry<String, List<VCardProperty>> entry : propertiesByGroup) { 299 String groupName = entry.getKey(); 300 if (groupName != null) { 301 AttributesImpl attr = new AttributesImpl(); 302 attr.addAttribute(XCardQNames.NAMESPACE, "", "name", "", groupName); 303 304 start(GROUP, attr); 305 } 306 307 for (VCardProperty property : entry.getValue()) { 308 write(property, vcard); 309 } 310 311 if (groupName != null) { 312 end(GROUP); 313 } 314 } 315 316 end(VCARD); 317 } catch (SAXException e) { 318 throw new IOException(e); 319 } 320 } 321 322 /** 323 * Terminates the XML document and closes the output stream. 324 */ 325 public void close() throws IOException { 326 try { 327 if (!started) { 328 handler.startDocument(); 329 330 if (!vcardsElementExists) { 331 //don't output a <vcards> element if the parent is a <vcards> element 332 start(VCARDS); 333 } 334 } 335 336 if (!vcardsElementExists) { 337 end(VCARDS); 338 } 339 handler.endDocument(); 340 } catch (SAXException e) { 341 throw new IOException(e); 342 } 343 344 if (writer != null) { 345 writer.close(); 346 } 347 } 348 349 @SuppressWarnings({ "rawtypes", "unchecked" }) 350 private void write(VCardProperty property, VCard vcard) throws SAXException { 351 VCardPropertyScribe scribe = index.getPropertyScribe(property); 352 VCardParameters parameters = scribe.prepareParameters(property, targetVersion, vcard); 353 354 removeUnsupportedParameters(parameters); 355 356 //get the property element to write 357 Element propertyElement; 358 if (property instanceof Xml) { 359 Xml xml = (Xml) property; 360 Document value = xml.getValue(); 361 if (value == null) { 362 return; 363 } 364 propertyElement = value.getDocumentElement(); 365 } else { 366 QName qname = scribe.getQName(); 367 propertyElement = DOC.createElementNS(qname.getNamespaceURI(), qname.getLocalPart()); 368 try { 369 scribe.writeXml(property, propertyElement); 370 } catch (SkipMeException | EmbeddedVCardException e) { 371 return; 372 } 373 } 374 375 start(propertyElement); 376 377 write(parameters); 378 write(propertyElement); 379 380 end(propertyElement); 381 } 382 383 private void write(Element propertyElement) throws SAXException { 384 NodeList children = propertyElement.getChildNodes(); 385 for (Node child : XmlUtils.iterable(children)) { 386 if (child instanceof Element) { 387 Element element = (Element) child; 388 389 if (element.hasChildNodes()) { 390 start(element); 391 write(element); 392 end(element); 393 } else { 394 childless(element); 395 } 396 397 continue; 398 } 399 400 if (child instanceof Text) { 401 Text text = (Text) child; 402 text(text.getTextContent()); 403 continue; 404 } 405 } 406 } 407 408 private void write(VCardParameters parameters) throws SAXException { 409 if (parameters.isEmpty()) { 410 return; 411 } 412 413 start(PARAMETERS); 414 415 for (Map.Entry<String, List<String>> parameter : parameters) { 416 String parameterName = parameter.getKey().toLowerCase(); 417 start(parameterName); 418 419 for (String parameterValue : parameter.getValue()) { 420 VCardDataType dataType = parameterDataTypes.get(parameterName); 421 String dataTypeElementName = (dataType == null) ? "unknown" : dataType.getName().toLowerCase(); 422 423 start(dataTypeElementName); 424 text(parameterValue); 425 end(dataTypeElementName); 426 } 427 428 end(parameterName); 429 } 430 431 end(PARAMETERS); 432 } 433 434 /** 435 * Makes an childless element appear as {@code <foo />} instead of 436 * {@code <foo></foo>} 437 * @param element the element 438 * @throws SAXException if there is a problem creating the element 439 */ 440 private void childless(Element element) throws SAXException { 441 Attributes attributes = getElementAttributes(element); 442 handler.startElement(element.getNamespaceURI(), "", element.getLocalName(), attributes); 443 handler.endElement(element.getNamespaceURI(), "", element.getLocalName()); 444 } 445 446 private void start(Element element) throws SAXException { 447 Attributes attributes = getElementAttributes(element); 448 start(element.getNamespaceURI(), element.getLocalName(), attributes); 449 } 450 451 private void start(String element) throws SAXException { 452 start(element, new AttributesImpl()); 453 } 454 455 private void start(QName qname) throws SAXException { 456 start(qname, new AttributesImpl()); 457 } 458 459 private void start(QName qname, Attributes attributes) throws SAXException { 460 start(qname.getNamespaceURI(), qname.getLocalPart(), attributes); 461 } 462 463 private void start(String element, Attributes attributes) throws SAXException { 464 start(targetVersion.getXmlNamespace(), element, attributes); 465 } 466 467 private void start(String namespace, String element, Attributes attributes) throws SAXException { 468 handler.startElement(namespace, "", element, attributes); 469 } 470 471 private void end(Element element) throws SAXException { 472 end(element.getNamespaceURI(), element.getLocalName()); 473 } 474 475 private void end(String element) throws SAXException { 476 end(targetVersion.getXmlNamespace(), element); 477 } 478 479 private void end(QName qname) throws SAXException { 480 end(qname.getNamespaceURI(), qname.getLocalPart()); 481 } 482 483 private void end(String namespace, String element) throws SAXException { 484 handler.endElement(namespace, "", element); 485 } 486 487 private void text(String text) throws SAXException { 488 handler.characters(text.toCharArray(), 0, text.length()); 489 } 490 491 private Attributes getElementAttributes(Element element) { 492 AttributesImpl attributes = new AttributesImpl(); 493 NamedNodeMap attributeNodes = element.getAttributes(); 494 495 //@formatter:off 496 XmlUtils.stream(attributeNodes) 497 .filter(node -> !"xmlns".equals(node.getLocalName())) 498 .forEach(node -> attributes.addAttribute(node.getNamespaceURI(), "", node.getLocalName(), "", node.getNodeValue())); 499 //@formatter:on 500 501 return attributes; 502 } 503}