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