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-2023, 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 for (Map.Entry<String, String> entry : outputProperties.entrySet()) { 262 String key = entry.getKey(); 263 String value = entry.getValue(); 264 transformer.setOutputProperty(key, value); 265 } 266 267 Result result = (writer == null) ? new DOMResult(parent) : new StreamResult(writer); 268 handler.setResult(result); 269 } 270 271 private boolean isVCardsElement(Node node) { 272 if (node == null) { 273 return false; 274 } 275 276 if (!(node instanceof Element)) { 277 return false; 278 } 279 280 return XmlUtils.hasQName(node, VCARDS); 281 } 282 283 @Override 284 protected void _write(VCard vcard, List<VCardProperty> properties) throws IOException { 285 try { 286 if (!started) { 287 handler.startDocument(); 288 289 if (!vcardsElementExists) { 290 //don't output a <vcards> element if the parent is a <vcards> element 291 start(VCARDS); 292 } 293 294 started = true; 295 } 296 297 ListMultimap<String, VCardProperty> propertiesByGroup = new ListMultimap<>(); //group the types by group name (null = no group name) 298 for (VCardProperty property : properties) { 299 propertiesByGroup.put(property.getGroup(), property); 300 } 301 302 start(VCARD); 303 304 for (Map.Entry<String, List<VCardProperty>> entry : propertiesByGroup) { 305 String groupName = entry.getKey(); 306 if (groupName != null) { 307 AttributesImpl attr = new AttributesImpl(); 308 attr.addAttribute(XCardQNames.NAMESPACE, "", "name", "", groupName); 309 310 start(GROUP, attr); 311 } 312 313 for (VCardProperty property : entry.getValue()) { 314 write(property, vcard); 315 } 316 317 if (groupName != null) { 318 end(GROUP); 319 } 320 } 321 322 end(VCARD); 323 } catch (SAXException e) { 324 throw new IOException(e); 325 } 326 } 327 328 /** 329 * Terminates the XML document and closes the output stream. 330 */ 331 public void close() throws IOException { 332 try { 333 if (!started) { 334 handler.startDocument(); 335 336 if (!vcardsElementExists) { 337 //don't output a <vcards> element if the parent is a <vcards> element 338 start(VCARDS); 339 } 340 } 341 342 if (!vcardsElementExists) { 343 end(VCARDS); 344 } 345 handler.endDocument(); 346 } catch (SAXException e) { 347 throw new IOException(e); 348 } 349 350 if (writer != null) { 351 writer.close(); 352 } 353 } 354 355 @SuppressWarnings({ "rawtypes", "unchecked" }) 356 private void write(VCardProperty property, VCard vcard) throws SAXException { 357 VCardPropertyScribe scribe = index.getPropertyScribe(property); 358 VCardParameters parameters = scribe.prepareParameters(property, targetVersion, vcard); 359 360 removeUnsupportedParameters(parameters); 361 362 //get the property element to write 363 Element propertyElement; 364 if (property instanceof Xml) { 365 Xml xml = (Xml) property; 366 Document value = xml.getValue(); 367 if (value == null) { 368 return; 369 } 370 propertyElement = value.getDocumentElement(); 371 } else { 372 QName qname = scribe.getQName(); 373 propertyElement = DOC.createElementNS(qname.getNamespaceURI(), qname.getLocalPart()); 374 try { 375 scribe.writeXml(property, propertyElement); 376 } catch (SkipMeException e) { 377 return; 378 } catch (EmbeddedVCardException e) { 379 return; 380 } 381 } 382 383 start(propertyElement); 384 385 write(parameters); 386 write(propertyElement); 387 388 end(propertyElement); 389 } 390 391 private void write(Element propertyElement) throws SAXException { 392 NodeList children = propertyElement.getChildNodes(); 393 for (int i = 0; i < children.getLength(); i++) { 394 Node child = children.item(i); 395 396 if (child instanceof Element) { 397 Element element = (Element) child; 398 399 if (element.hasChildNodes()) { 400 start(element); 401 write(element); 402 end(element); 403 } else { 404 childless(element); 405 } 406 407 continue; 408 } 409 410 if (child instanceof Text) { 411 Text text = (Text) child; 412 text(text.getTextContent()); 413 continue; 414 } 415 } 416 } 417 418 private void write(VCardParameters parameters) throws SAXException { 419 if (parameters.isEmpty()) { 420 return; 421 } 422 423 start(PARAMETERS); 424 425 for (Map.Entry<String, List<String>> parameter : parameters) { 426 String parameterName = parameter.getKey().toLowerCase(); 427 start(parameterName); 428 429 for (String parameterValue : parameter.getValue()) { 430 VCardDataType dataType = parameterDataTypes.get(parameterName); 431 String dataTypeElementName = (dataType == null) ? "unknown" : dataType.getName().toLowerCase(); 432 433 start(dataTypeElementName); 434 text(parameterValue); 435 end(dataTypeElementName); 436 } 437 438 end(parameterName); 439 } 440 441 end(PARAMETERS); 442 } 443 444 /** 445 * Makes an childless element appear as {@code <foo />} instead of 446 * {@code <foo></foo>} 447 * @param element the element 448 * @throws SAXException if there is a problem creating the element 449 */ 450 private void childless(Element element) throws SAXException { 451 Attributes attributes = getElementAttributes(element); 452 handler.startElement(element.getNamespaceURI(), "", element.getLocalName(), attributes); 453 handler.endElement(element.getNamespaceURI(), "", element.getLocalName()); 454 } 455 456 private void start(Element element) throws SAXException { 457 Attributes attributes = getElementAttributes(element); 458 start(element.getNamespaceURI(), element.getLocalName(), attributes); 459 } 460 461 private void start(String element) throws SAXException { 462 start(element, new AttributesImpl()); 463 } 464 465 private void start(QName qname) throws SAXException { 466 start(qname, new AttributesImpl()); 467 } 468 469 private void start(QName qname, Attributes attributes) throws SAXException { 470 start(qname.getNamespaceURI(), qname.getLocalPart(), attributes); 471 } 472 473 private void start(String element, Attributes attributes) throws SAXException { 474 start(targetVersion.getXmlNamespace(), element, attributes); 475 } 476 477 private void start(String namespace, String element, Attributes attributes) throws SAXException { 478 handler.startElement(namespace, "", element, attributes); 479 } 480 481 private void end(Element element) throws SAXException { 482 end(element.getNamespaceURI(), element.getLocalName()); 483 } 484 485 private void end(String element) throws SAXException { 486 end(targetVersion.getXmlNamespace(), element); 487 } 488 489 private void end(QName qname) throws SAXException { 490 end(qname.getNamespaceURI(), qname.getLocalPart()); 491 } 492 493 private void end(String namespace, String element) throws SAXException { 494 handler.endElement(namespace, "", element); 495 } 496 497 private void text(String text) throws SAXException { 498 handler.characters(text.toCharArray(), 0, text.length()); 499 } 500 501 private Attributes getElementAttributes(Element element) { 502 AttributesImpl attributes = new AttributesImpl(); 503 NamedNodeMap attributeNodes = element.getAttributes(); 504 for (int i = 0; i < attributeNodes.getLength(); i++) { 505 Node node = attributeNodes.item(i); 506 507 String localName = node.getLocalName(); 508 if ("xmlns".equals(localName)) { 509 continue; 510 } 511 512 attributes.addAttribute(node.getNamespaceURI(), "", localName, "", node.getNodeValue()); 513 } 514 return attributes; 515 } 516}