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.File; 009import java.io.IOException; 010import java.io.OutputStream; 011import java.io.Writer; 012import java.util.Collections; 013import java.util.List; 014import java.util.Map; 015 016import javax.xml.namespace.QName; 017import javax.xml.transform.Result; 018import javax.xml.transform.Transformer; 019import javax.xml.transform.TransformerConfigurationException; 020import javax.xml.transform.TransformerFactory; 021import javax.xml.transform.dom.DOMResult; 022import javax.xml.transform.sax.SAXTransformerFactory; 023import javax.xml.transform.sax.TransformerHandler; 024import javax.xml.transform.stream.StreamResult; 025 026import org.w3c.dom.Document; 027import org.w3c.dom.Element; 028import org.w3c.dom.NamedNodeMap; 029import org.w3c.dom.Node; 030import org.w3c.dom.NodeList; 031import org.w3c.dom.Text; 032import org.xml.sax.Attributes; 033import org.xml.sax.SAXException; 034import org.xml.sax.helpers.AttributesImpl; 035 036import ezvcard.VCard; 037import ezvcard.VCardDataType; 038import ezvcard.io.EmbeddedVCardException; 039import ezvcard.io.SkipMeException; 040import ezvcard.io.scribe.VCardPropertyScribe; 041import ezvcard.parameter.VCardParameters; 042import ezvcard.property.VCardProperty; 043import ezvcard.property.Xml; 044import ezvcard.util.ListMultimap; 045import ezvcard.util.Utf8Writer; 046import ezvcard.util.XmlUtils; 047 048/* 049 Copyright (c) 2012-2018, Michael Angstadt 050 All rights reserved. 051 052 Redistribution and use in source and binary forms, with or without 053 modification, are permitted provided that the following conditions are met: 054 055 1. Redistributions of source code must retain the above copyright notice, this 056 list of conditions and the following disclaimer. 057 2. Redistributions in binary form must reproduce the above copyright notice, 058 this list of conditions and the following disclaimer in the documentation 059 and/or other materials provided with the distribution. 060 061 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 062 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 063 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 064 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 065 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 066 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 067 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 068 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 069 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 070 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 071 072 The views and conclusions contained in the software and documentation are those 073 of the authors and should not be interpreted as representing official policies, 074 either expressed or implied, of the FreeBSD Project. 075 */ 076 077/** 078 * <p> 079 * Writes xCards (XML-encoded vCards) in a streaming fashion. 080 * </p> 081 * <p> 082 * <b>Example:</b> 083 * </p> 084 * 085 * <pre class="brush:java"> 086 * VCard vcard1 = ... 087 * VCard vcard2 = ... 088 * File file = new File("vcards.xml"); 089 * XCardWriter writer = null; 090 * try { 091 * writer = new XCardWriter(file); 092 * writer.write(vcard1); 093 * writer.write(vcard2); 094 * } finally { 095 * if (writer != null) writer.close(); 096 * } 097 * </pre> 098 * @author Michael Angstadt 099 * @see <a href="http://tools.ietf.org/html/rfc6351">RFC 6351</a> 100 */ 101public class XCardWriter extends XCardWriterBase { 102 //How to use SAX to write XML: http://stackoverflow.com/q/4898590 103 104 private final Document DOC = XmlUtils.createDocument(); 105 106 private final Writer writer; 107 private final TransformerHandler handler; 108 private final boolean vcardsElementExists; 109 private boolean started = false; 110 111 /** 112 * @param out the output stream to write to (UTF-8 encoding will be used) 113 */ 114 public XCardWriter(OutputStream out) { 115 this(out, (Integer) null); 116 } 117 118 /** 119 * @param out the output stream to write to (UTF-8 encoding will be used) 120 * @param indent the number of indent spaces to use for pretty-printing or 121 * null to disable pretty-printing (disabled by default) 122 */ 123 public XCardWriter(OutputStream out, Integer indent) { 124 this(out, indent, null); 125 } 126 127 /** 128 * @param out the output stream to write to (UTF-8 encoding will be used) 129 * @param indent the number of indent spaces to use for pretty-printing or 130 * null to disable pretty-printing (disabled by default) 131 * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many 132 * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library 133 * like <a href= 134 * "http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22xalan%22%20AND%20a%3A%22xalan%22" 135 * >xalan</a> to your project) 136 */ 137 public XCardWriter(OutputStream out, Integer indent, String xmlVersion) { 138 this(out, new XCardOutputProperties(indent, xmlVersion)); 139 } 140 141 /** 142 * @param out the output stream to write to (UTF-8 encoding will be used) 143 * @param outputProperties properties to assign to the JAXP transformer (see 144 * {@link Transformer#setOutputProperty}) 145 */ 146 public XCardWriter(OutputStream out, Map<String, String> outputProperties) { 147 this(new Utf8Writer(out), outputProperties); 148 } 149 150 /** 151 * @param file the file to write to (UTF-8 encoding will be used) 152 * @throws IOException if there's a problem opening the file 153 */ 154 public XCardWriter(File file) throws IOException { 155 this(file, (Integer) null); 156 } 157 158 /** 159 * @param file the file to write to (UTF-8 encoding will be used) 160 * @param indent the number of indent spaces to use for pretty-printing or 161 * null to disable pretty-printing (disabled by default) 162 * @throws IOException if there's a problem opening the file 163 */ 164 public XCardWriter(File file, Integer indent) throws IOException { 165 this(file, indent, null); 166 } 167 168 /** 169 * @param file the file to write to (UTF-8 encoding will be used) 170 * @param indent the number of indent spaces to use for pretty-printing or 171 * null to disable pretty-printing (disabled by default) 172 * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many 173 * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library 174 * like <a href= 175 * "http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22xalan%22%20AND%20a%3A%22xalan%22" 176 * >xalan</a> to your project) 177 * @throws IOException if there's a problem opening the file 178 */ 179 public XCardWriter(File file, Integer indent, String xmlVersion) throws IOException { 180 this(file, new XCardOutputProperties(indent, xmlVersion)); 181 } 182 183 /** 184 * @param file the file to write to (UTF-8 encoding will be used) 185 * @param outputProperties properties to assign to the JAXP transformer (see 186 * {@link Transformer#setOutputProperty}) 187 * @throws IOException if there's a problem opening the file 188 */ 189 public XCardWriter(File file, Map<String, String> outputProperties) throws IOException { 190 this(new Utf8Writer(file), outputProperties); 191 } 192 193 /** 194 * @param writer the writer to write to 195 */ 196 public XCardWriter(Writer writer) { 197 this(writer, (Integer) null); 198 } 199 200 /** 201 * @param writer the writer to write to 202 * @param indent the number of indent spaces to use for pretty-printing or 203 * null to disable pretty-printing (disabled by default) 204 */ 205 public XCardWriter(Writer writer, Integer indent) { 206 this(writer, indent, null); 207 } 208 209 /** 210 * @param writer the writer to write to 211 * @param indent the number of indent spaces to use for pretty-printing or 212 * null to disable pretty-printing (disabled by default) 213 * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many 214 * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library 215 * like <a href= 216 * "http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22xalan%22%20AND%20a%3A%22xalan%22" 217 * >xalan</a> to your project) 218 */ 219 public XCardWriter(Writer writer, Integer indent, String xmlVersion) { 220 this(writer, new XCardOutputProperties(indent, xmlVersion)); 221 } 222 223 /** 224 * @param writer the writer to write to 225 * @param outputProperties properties to assign to the JAXP transformer (see 226 * {@link Transformer#setOutputProperty}) 227 */ 228 public XCardWriter(Writer writer, Map<String, String> outputProperties) { 229 this(writer, null, outputProperties); 230 } 231 232 /** 233 * @param parent the DOM node to add child elements to 234 */ 235 public XCardWriter(Node parent) { 236 this(null, parent, Collections.<String, String> emptyMap()); 237 } 238 239 private XCardWriter(Writer writer, Node parent, Map<String, String> outputProperties) { 240 this.writer = writer; 241 242 if (parent instanceof Document) { 243 Node root = ((Document) parent).getDocumentElement(); 244 if (root != null) { 245 parent = root; 246 } 247 } 248 this.vcardsElementExists = isVCardsElement(parent); 249 250 try { 251 SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance(); 252 handler = factory.newTransformerHandler(); 253 } catch (TransformerConfigurationException e) { 254 throw new RuntimeException(e); 255 } 256 257 Transformer transformer = handler.getTransformer(); 258 259 /* 260 * Using Transformer#setOutputProperties(Properties) doesn't work for 261 * some reason for setting the number of indentation spaces. 262 */ 263 for (Map.Entry<String, String> entry : outputProperties.entrySet()) { 264 String key = entry.getKey(); 265 String value = entry.getValue(); 266 transformer.setOutputProperty(key, value); 267 } 268 269 Result result = (writer == null) ? new DOMResult(parent) : new StreamResult(writer); 270 handler.setResult(result); 271 } 272 273 private boolean isVCardsElement(Node node) { 274 if (node == null) { 275 return false; 276 } 277 278 if (!(node instanceof Element)) { 279 return false; 280 } 281 282 return XmlUtils.hasQName(node, VCARDS); 283 } 284 285 @Override 286 protected void _write(VCard vcard, List<VCardProperty> properties) throws IOException { 287 try { 288 if (!started) { 289 handler.startDocument(); 290 291 if (!vcardsElementExists) { 292 //don't output a <vcards> element if the parent is a <vcards> element 293 start(VCARDS); 294 } 295 296 started = true; 297 } 298 299 ListMultimap<String, VCardProperty> propertiesByGroup = new ListMultimap<String, VCardProperty>(); //group the types by group name (null = no group name) 300 for (VCardProperty property : properties) { 301 propertiesByGroup.put(property.getGroup(), property); 302 } 303 304 start(VCARD); 305 306 for (Map.Entry<String, List<VCardProperty>> entry : propertiesByGroup) { 307 String groupName = entry.getKey(); 308 if (groupName != null) { 309 AttributesImpl attr = new AttributesImpl(); 310 attr.addAttribute(XCardQNames.NAMESPACE, "", "name", "", groupName); 311 312 start(GROUP, attr); 313 } 314 315 for (VCardProperty property : entry.getValue()) { 316 write(property, vcard); 317 } 318 319 if (groupName != null) { 320 end(GROUP); 321 } 322 } 323 324 end(VCARD); 325 } catch (SAXException e) { 326 throw new IOException(e); 327 } 328 } 329 330 /** 331 * Terminates the XML document and closes the output stream. 332 */ 333 public void close() throws IOException { 334 try { 335 if (!started) { 336 handler.startDocument(); 337 338 if (!vcardsElementExists) { 339 //don't output a <vcards> element if the parent is a <vcards> element 340 start(VCARDS); 341 } 342 } 343 344 if (!vcardsElementExists) { 345 end(VCARDS); 346 } 347 handler.endDocument(); 348 } catch (SAXException e) { 349 throw new IOException(e); 350 } 351 352 if (writer != null) { 353 writer.close(); 354 } 355 } 356 357 @SuppressWarnings({ "rawtypes", "unchecked" }) 358 private void write(VCardProperty property, VCard vcard) throws SAXException { 359 VCardPropertyScribe scribe = index.getPropertyScribe(property); 360 VCardParameters parameters = scribe.prepareParameters(property, targetVersion, vcard); 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 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}