001package biweekly.io.xml; 002 003import static biweekly.io.xml.XCalNamespaceContext.XCAL_NS; 004import static biweekly.io.xml.XCalQNames.COMPONENTS; 005import static biweekly.io.xml.XCalQNames.ICALENDAR; 006import static biweekly.io.xml.XCalQNames.PARAMETERS; 007import static biweekly.io.xml.XCalQNames.PROPERTIES; 008import static biweekly.util.IOUtils.utf8Writer; 009import static biweekly.util.StringUtils.NEWLINE; 010 011import java.io.Closeable; 012import java.io.File; 013import java.io.IOException; 014import java.io.OutputStream; 015import java.io.Writer; 016import java.util.Collection; 017import java.util.HashMap; 018import java.util.List; 019import java.util.Map; 020 021import javax.xml.namespace.QName; 022import javax.xml.transform.Result; 023import javax.xml.transform.TransformerConfigurationException; 024import javax.xml.transform.TransformerFactory; 025import javax.xml.transform.dom.DOMResult; 026import javax.xml.transform.sax.SAXTransformerFactory; 027import javax.xml.transform.sax.TransformerHandler; 028import javax.xml.transform.stream.StreamResult; 029 030import org.w3c.dom.Document; 031import org.w3c.dom.Element; 032import org.w3c.dom.NamedNodeMap; 033import org.w3c.dom.Node; 034import org.w3c.dom.NodeList; 035import org.w3c.dom.Text; 036import org.xml.sax.Attributes; 037import org.xml.sax.SAXException; 038import org.xml.sax.helpers.AttributesImpl; 039 040import biweekly.ICalDataType; 041import biweekly.ICalendar; 042import biweekly.component.ICalComponent; 043import biweekly.io.SkipMeException; 044import biweekly.io.scribe.ScribeIndex; 045import biweekly.io.scribe.component.ICalComponentScribe; 046import biweekly.io.scribe.property.ICalPropertyScribe; 047import biweekly.parameter.ICalParameters; 048import biweekly.property.ICalProperty; 049import biweekly.property.Xml; 050import biweekly.util.XmlUtils; 051 052/* 053 Copyright (c) 2013, Michael Angstadt 054 All rights reserved. 055 056 Redistribution and use in source and binary forms, with or without 057 modification, are permitted provided that the following conditions are met: 058 059 1. Redistributions of source code must retain the above copyright notice, this 060 list of conditions and the following disclaimer. 061 2. Redistributions in binary form must reproduce the above copyright notice, 062 this list of conditions and the following disclaimer in the documentation 063 and/or other materials provided with the distribution. 064 065 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 066 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 067 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 068 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 069 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 070 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 071 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 072 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 073 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 074 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 075 076 The views and conclusions contained in the software and documentation are those 077 of the authors and should not be interpreted as representing official policies, 078 either expressed or implied, of the FreeBSD Project. 079 */ 080 081/** 082 * <p> 083 * Writes xCards (XML-encoded vCards) in a streaming fashion. 084 * </p> 085 * <p> 086 * <b>Example:</b> 087 * 088 * <pre class="brush:java"> 089 * VCard vcard1 = ... 090 * VCard vcard2 = ... 091 * 092 * File file = new File("vcards.xml"); 093 * XCardWriter xcardWriter = new XCardWriter(file); 094 * xcardWriter.write(vcard1); 095 * xcardWriter.write(vcard2); 096 * xcardWriter.close(); 097 * </pre> 098 * @author Michael Angstadt 099 * @see <a href="http://tools.ietf.org/html/rfc6351">RFC 6351</a> 100 */ 101public class XCalWriter implements Closeable { 102 //how to use SAX to write XML: http://stackoverflow.com/questions/4898590/generating-xml-using-sax-and-java 103 private final Document DOC = XmlUtils.createDocument(); 104 105 /** 106 * Defines the names of the XML elements that are used to hold each 107 * parameter's value. 108 */ 109 private final Map<String, ICalDataType> parameterDataTypes = new HashMap<String, ICalDataType>(); 110 { 111 registerParameterDataType(ICalParameters.CN, ICalDataType.TEXT); 112 registerParameterDataType(ICalParameters.ALTREP, ICalDataType.URI); 113 registerParameterDataType(ICalParameters.CUTYPE, ICalDataType.TEXT); 114 registerParameterDataType(ICalParameters.DELEGATED_FROM, ICalDataType.CAL_ADDRESS); 115 registerParameterDataType(ICalParameters.DELEGATED_TO, ICalDataType.CAL_ADDRESS); 116 registerParameterDataType(ICalParameters.DIR, ICalDataType.URI); 117 registerParameterDataType(ICalParameters.ENCODING, ICalDataType.TEXT); 118 registerParameterDataType(ICalParameters.FMTTYPE, ICalDataType.TEXT); 119 registerParameterDataType(ICalParameters.FBTYPE, ICalDataType.TEXT); 120 registerParameterDataType(ICalParameters.LANGUAGE, ICalDataType.TEXT); 121 registerParameterDataType(ICalParameters.MEMBER, ICalDataType.CAL_ADDRESS); 122 registerParameterDataType(ICalParameters.PARTSTAT, ICalDataType.TEXT); 123 registerParameterDataType(ICalParameters.RANGE, ICalDataType.TEXT); 124 registerParameterDataType(ICalParameters.RELATED, ICalDataType.TEXT); 125 registerParameterDataType(ICalParameters.RELTYPE, ICalDataType.TEXT); 126 registerParameterDataType(ICalParameters.ROLE, ICalDataType.TEXT); 127 registerParameterDataType(ICalParameters.RSVP, ICalDataType.BOOLEAN); 128 registerParameterDataType(ICalParameters.SENT_BY, ICalDataType.CAL_ADDRESS); 129 registerParameterDataType(ICalParameters.TZID, ICalDataType.TEXT); 130 } 131 132 private final Writer writer; 133 private final TransformerHandler handler; 134 private final String indent; 135 private final boolean icalendarElementExists; 136 private int level = 0; 137 private boolean textNodeJustPrinted = false, started = false; 138 private ScribeIndex index = new ScribeIndex(); 139 140 /** 141 * Creates an xCard writer (UTF-8 encoding will be used). 142 * @param out the output stream to write the xCards to 143 */ 144 public XCalWriter(OutputStream out) { 145 this(utf8Writer(out)); 146 } 147 148 /** 149 * Creates an xCard writer (UTF-8 encoding will be used). 150 * @param out the output stream to write the xCards to 151 * @param indent the indentation string to use for pretty printing (e.g. 152 * "\t") or null not to pretty print 153 */ 154 public XCalWriter(OutputStream out, String indent) { 155 this(utf8Writer(out), indent); 156 } 157 158 /** 159 * Creates an xCard writer (UTF-8 encoding will be used). 160 * @param file the file to write the xCards to 161 * @throws IOException if there's a problem opening the file 162 */ 163 public XCalWriter(File file) throws IOException { 164 this(utf8Writer(file)); 165 } 166 167 /** 168 * Creates an xCard writer (UTF-8 encoding will be used). 169 * @param file the file to write the xCards to 170 * @param indent the indentation string to use for pretty printing (e.g. 171 * "\t") or null not to pretty print 172 * @throws IOException if there's a problem opening the file 173 */ 174 public XCalWriter(File file, String indent) throws IOException { 175 this(utf8Writer(file), indent); 176 } 177 178 /** 179 * Creates an xCard writer. 180 * @param writer the writer to write to 181 */ 182 public XCalWriter(Writer writer) { 183 this(writer, null); 184 } 185 186 /** 187 * Creates an xCard writer. 188 * @param writer the writer to write to 189 * @param indent the indentation string to use for pretty printing (e.g. 190 * "\t") or null not to pretty print 191 */ 192 public XCalWriter(Writer writer, String indent) { 193 this(writer, indent, null); 194 } 195 196 /** 197 * Creates an xCard writer. 198 * @param parent the DOM node to add child elements to 199 */ 200 public XCalWriter(Node parent) { 201 this(null, null, parent); 202 } 203 204 private XCalWriter(Writer writer, String indent, Node parent) { 205 this.writer = writer; 206 this.indent = indent; 207 208 if (parent instanceof Document) { 209 Node root = parent.getFirstChild(); 210 if (root != null) { 211 parent = root; 212 } 213 } 214 this.icalendarElementExists = isICalendarElement(parent); 215 216 try { 217 SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance(); 218 handler = factory.newTransformerHandler(); 219 } catch (TransformerConfigurationException e) { 220 throw new RuntimeException(e); 221 } 222 223 Result result = (writer == null) ? new DOMResult(parent) : new StreamResult(writer); 224 handler.setResult(result); 225 } 226 227 private boolean isICalendarElement(Node node) { 228 if (node == null) { 229 return false; 230 } 231 232 if (!(node instanceof Element)) { 233 return false; 234 } 235 236 return ICALENDAR.getNamespaceURI().equals(node.getNamespaceURI()) && ICALENDAR.getLocalPart().equals(node.getLocalName()); 237 } 238 239 /** 240 * <p> 241 * Registers an experimental property scribe. Can also be used to override 242 * the scribe of a standard property (such as DTSTART). Calling this method 243 * is the same as calling: 244 * </p> 245 * <p> 246 * {@code getScribeIndex().register(scribe)}. 247 * </p> 248 * @param scribe the scribe to register 249 */ 250 public void registerScribe(ICalPropertyScribe<? extends ICalProperty> scribe) { 251 index.register(scribe); 252 } 253 254 /** 255 * <p> 256 * Registers an experimental component scribe. Can also be used to override 257 * the scribe of a standard component (such as VEVENT). Calling this method 258 * is the same as calling: 259 * </p> 260 * <p> 261 * {@code getScribeIndex().register(scribe)}. 262 * </p> 263 * @param scribe the scribe to register 264 */ 265 public void registerScribe(ICalComponentScribe<? extends ICalComponent> scribe) { 266 index.register(scribe); 267 } 268 269 /** 270 * Gets the object that manages the component/property scribes. 271 * @return the scribe index 272 */ 273 public ScribeIndex getScribeIndex() { 274 return index; 275 } 276 277 /** 278 * Sets the object that manages the component/property scribes. 279 * @param scribe the scribe index 280 */ 281 public void setScribeIndex(ScribeIndex scribe) { 282 this.index = scribe; 283 } 284 285 /** 286 * Writes an iCalendar object. 287 * @param ical the iCalendar object to write 288 * @throws SAXException if there's a problem writing the iCalendar object 289 * @throws IllegalArgumentException if the scribe index is missing scribes 290 * for one or more properties/components. 291 */ 292 public void write(ICalendar ical) throws SAXException { 293 index.hasScribesFor(ical); 294 295 if (!started) { 296 handler.startDocument(); 297 298 if (!icalendarElementExists) { 299 //don't output a <icalendar> element if the parent is a <icalendar> element 300 start(ICALENDAR); 301 level++; 302 } 303 304 started = true; 305 } 306 307 write((ICalComponent) ical); 308 } 309 310 /** 311 * Registers the data type of an experimental parameter. Experimental 312 * parameters use the "unknown" data type by default. 313 * @param parameterName the parameter name (e.g. "x-foo") 314 * @param dataType the data type or null to remove 315 */ 316 public void registerParameterDataType(String parameterName, ICalDataType dataType) { 317 parameterName = parameterName.toLowerCase(); 318 if (dataType == null) { 319 parameterDataTypes.remove(parameterName); 320 } else { 321 parameterDataTypes.put(parameterName, dataType); 322 } 323 } 324 325 @SuppressWarnings({ "rawtypes", "unchecked" }) 326 private void write(ICalComponent component) throws SAXException { 327 ICalComponentScribe scribe = index.getComponentScribe(component); 328 String name = scribe.getComponentName().toLowerCase(); 329 330 start(name); 331 level++; 332 333 List<ICalProperty> properties = scribe.getProperties(component); 334 if (!properties.isEmpty()) { 335 start(PROPERTIES); 336 level++; 337 338 for (Object propertyObj : scribe.getProperties(component)) { 339 ICalProperty property = (ICalProperty) propertyObj; 340 write(property); 341 } 342 343 level--; 344 end(PROPERTIES); 345 } 346 347 Collection<ICalComponent> components = scribe.getComponents(component); 348 if (!components.isEmpty()) { 349 start(COMPONENTS); 350 level++; 351 352 for (Object subComponentObj : scribe.getComponents(component)) { 353 ICalComponent subComponent = (ICalComponent) subComponentObj; 354 write(subComponent); 355 } 356 357 level--; 358 end(COMPONENTS); 359 } 360 361 level--; 362 end(name); 363 } 364 365 @SuppressWarnings({ "rawtypes", "unchecked" }) 366 private void write(ICalProperty property) throws SAXException { 367 ICalPropertyScribe scribe = index.getPropertyScribe(property); 368 ICalParameters parameters = scribe.prepareParameters(property); 369 370 //get the property element to write 371 Element propertyElement; 372 if (property instanceof Xml) { 373 Xml xml = (Xml) property; 374 Document value = xml.getValue(); 375 if (value == null) { 376 return; 377 } 378 propertyElement = XmlUtils.getRootElement(value); 379 } else { 380 QName qname = scribe.getQName(); 381 propertyElement = DOC.createElementNS(qname.getNamespaceURI(), qname.getLocalPart()); 382 try { 383 scribe.writeXml(property, propertyElement); 384 } catch (SkipMeException e) { 385 return; 386 } 387 } 388 389 start(propertyElement); 390 level++; 391 392 write(parameters); 393 write(propertyElement); 394 395 level--; 396 end(propertyElement); 397 } 398 399 private void write(Element propertyElement) throws SAXException { 400 NodeList children = propertyElement.getChildNodes(); 401 for (int i = 0; i < children.getLength(); i++) { 402 Node child = children.item(i); 403 404 if (child instanceof Element) { 405 Element element = (Element) child; 406 407 if (element.hasChildNodes()) { 408 start(element); 409 level++; 410 411 write(element); 412 413 level--; 414 end(element); 415 } else { 416 //make childless elements appear as "<foo />" instead of "<foo></foo>" 417 childless(element); 418 } 419 420 continue; 421 } 422 423 if (child instanceof Text) { 424 Text text = (Text) child; 425 text(text.getTextContent()); 426 continue; 427 } 428 } 429 } 430 431 private void write(ICalParameters parameters) throws SAXException { 432 if (parameters.isEmpty()) { 433 return; 434 } 435 436 start(PARAMETERS); 437 level++; 438 439 for (Map.Entry<String, List<String>> parameter : parameters) { 440 String parameterName = parameter.getKey().toLowerCase(); 441 start(parameterName); 442 level++; 443 444 for (String parameterValue : parameter.getValue()) { 445 ICalDataType dataType = parameterDataTypes.get(parameterName); 446 String dataTypeElementName = (dataType == null) ? "unknown" : dataType.getName().toLowerCase(); 447 448 start(dataTypeElementName); 449 text(parameterValue); 450 end(dataTypeElementName); 451 } 452 453 level--; 454 end(parameterName); 455 } 456 457 level--; 458 end(PARAMETERS); 459 } 460 461 private void indent() throws SAXException { 462 if (indent == null) { 463 return; 464 } 465 466 StringBuilder sb = new StringBuilder(NEWLINE); 467 for (int i = 0; i < level; i++) { 468 sb.append(indent); 469 } 470 471 String str = sb.toString(); 472 handler.ignorableWhitespace(str.toCharArray(), 0, str.length()); 473 } 474 475 private void childless(Element element) throws SAXException { 476 Attributes attributes = getElementAttributes(element); 477 indent(); 478 handler.startElement(element.getNamespaceURI(), "", element.getLocalName(), attributes); 479 handler.endElement(element.getNamespaceURI(), "", element.getLocalName()); 480 } 481 482 private void start(Element element) throws SAXException { 483 Attributes attributes = getElementAttributes(element); 484 start(element.getNamespaceURI(), element.getLocalName(), attributes); 485 } 486 487 private void start(String element) throws SAXException { 488 start(element, null); 489 } 490 491 private void start(QName qname) throws SAXException { 492 start(qname, null); 493 } 494 495 private void start(QName qname, Attributes attributes) throws SAXException { 496 start(qname.getNamespaceURI(), qname.getLocalPart(), attributes); 497 } 498 499 private void start(String element, Attributes attributes) throws SAXException { 500 start(XCAL_NS, element, attributes); 501 } 502 503 private void start(String namespace, String element, Attributes attributes) throws SAXException { 504 indent(); 505 handler.startElement(namespace, "", element, attributes); 506 } 507 508 private void end(Element element) throws SAXException { 509 end(element.getNamespaceURI(), element.getLocalName()); 510 } 511 512 private void end(String element) throws SAXException { 513 end(XCAL_NS, element); 514 } 515 516 private void end(QName qname) throws SAXException { 517 end(qname.getNamespaceURI(), qname.getLocalPart()); 518 } 519 520 private void end(String namespace, String element) throws SAXException { 521 if (!textNodeJustPrinted) { 522 indent(); 523 } 524 525 handler.endElement(namespace, "", element); 526 textNodeJustPrinted = false; 527 } 528 529 private void text(String text) throws SAXException { 530 handler.characters(text.toCharArray(), 0, text.length()); 531 textNodeJustPrinted = true; 532 } 533 534 private Attributes getElementAttributes(Element element) { 535 AttributesImpl attributes = new AttributesImpl(); 536 NamedNodeMap attributeNodes = element.getAttributes(); 537 for (int i = 0; i < attributeNodes.getLength(); i++) { 538 Node node = attributeNodes.item(i); 539 attributes.addAttribute(node.getNamespaceURI(), "", node.getLocalName(), "", node.getNodeValue()); 540 } 541 return attributes; 542 } 543 544 /** 545 * Terminates the XML document and closes the output stream. 546 */ 547 public void close() throws IOException { 548 try { 549 if (!started) { 550 handler.startDocument(); 551 552 if (!icalendarElementExists) { 553 //don't output a <icalendar> element if the parent is a <icalendar> element 554 start(ICALENDAR); 555 level++; 556 } 557 } 558 559 if (!icalendarElementExists) { 560 level--; 561 end(ICALENDAR); 562 } 563 handler.endDocument(); 564 } catch (SAXException e) { 565 throw new IOException(e); 566 } 567 568 if (writer != null) { 569 writer.close(); 570 } 571 } 572}