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; 009 010import java.io.File; 011import java.io.IOException; 012import java.io.OutputStream; 013import java.io.Writer; 014import java.util.Collection; 015import java.util.HashMap; 016import java.util.List; 017import java.util.Map; 018 019import javax.xml.namespace.QName; 020import javax.xml.transform.Result; 021import javax.xml.transform.TransformerConfigurationException; 022import javax.xml.transform.TransformerFactory; 023import javax.xml.transform.dom.DOMResult; 024import javax.xml.transform.sax.SAXTransformerFactory; 025import javax.xml.transform.sax.TransformerHandler; 026import javax.xml.transform.stream.StreamResult; 027 028import org.w3c.dom.Document; 029import org.w3c.dom.Element; 030import org.w3c.dom.NamedNodeMap; 031import org.w3c.dom.Node; 032import org.w3c.dom.NodeList; 033import org.w3c.dom.Text; 034import org.xml.sax.Attributes; 035import org.xml.sax.SAXException; 036import org.xml.sax.helpers.AttributesImpl; 037 038import biweekly.ICalDataType; 039import biweekly.ICalVersion; 040import biweekly.ICalendar; 041import biweekly.component.ICalComponent; 042import biweekly.component.VTimezone; 043import biweekly.io.SkipMeException; 044import biweekly.io.StreamWriter; 045import biweekly.io.scribe.component.ICalComponentScribe; 046import biweekly.io.scribe.property.ICalPropertyScribe; 047import biweekly.parameter.ICalParameters; 048import biweekly.property.ICalProperty; 049import biweekly.property.Version; 050import biweekly.property.Xml; 051import biweekly.util.StringUtils; 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 iCalendar objects) in a streaming fashion. 086 * </p> 087 * <p> 088 * <b>Example:</b> 089 * 090 * <pre class="brush:java"> 091 * ICalendar ical1 = ... 092 * ICalendar ical2 = ... 093 * File file = new File("icals.xml"); 094 * XCalWriter writer = null; 095 * try { 096 * writer = new XCalWriter(file); 097 * writer.write(ical1); 098 * writer.write(ical2); 099 * } finally { 100 * if (writer != null) writer.close(); 101 * } 102 * </pre> 103 * 104 * </p> 105 * 106 * <p> 107 * <b>Changing the timezone settings:</b> 108 * 109 * <pre class="brush:java"> 110 * XCalWriter writer = new XCalWriter(...); 111 * 112 * //format all date/time values in a specific timezone instead of UTC 113 * //note: this makes an HTTP call to the "tzurl.org" website 114 * writer.getTimezoneInfo().setDefaultTimeZone(TimeZone.getDefault()); 115 * 116 * //format the value of a particular date/time property in a specific timezone instead of UTC 117 * //note: this makes an HTTP call to the "tzurl.org" website 118 * DateStart dtstart = ... 119 * writer.getTimezoneInfo().setTimeZone(dtstart, TimeZone.getDefault()); 120 * 121 * //generate Outlook-friendly VTIMEZONE components: 122 * writer.getTimezoneInfo().setGenerator(new TzUrlDotOrgGenerator(true)); 123 * </pre> 124 * 125 * </p> 126 * 127 * @author Michael Angstadt 128 * @see <a href="http://tools.ietf.org/html/rfc6351">RFC 6351</a> 129 */ 130public class XCalWriter extends StreamWriter { 131 //How to use SAX to write XML: http://stackoverflow.com/q/4898590 132 private final Document DOC = XmlUtils.createDocument(); 133 134 /** 135 * Defines the names of the XML elements that are used to hold each 136 * parameter's value. 137 */ 138 private final Map<String, ICalDataType> parameterDataTypes = new HashMap<String, ICalDataType>(); 139 { 140 registerParameterDataType(ICalParameters.CN, ICalDataType.TEXT); 141 registerParameterDataType(ICalParameters.ALTREP, ICalDataType.URI); 142 registerParameterDataType(ICalParameters.CUTYPE, ICalDataType.TEXT); 143 registerParameterDataType(ICalParameters.DELEGATED_FROM, ICalDataType.CAL_ADDRESS); 144 registerParameterDataType(ICalParameters.DELEGATED_TO, ICalDataType.CAL_ADDRESS); 145 registerParameterDataType(ICalParameters.DIR, ICalDataType.URI); 146 registerParameterDataType(ICalParameters.ENCODING, ICalDataType.TEXT); 147 registerParameterDataType(ICalParameters.FMTTYPE, ICalDataType.TEXT); 148 registerParameterDataType(ICalParameters.FBTYPE, ICalDataType.TEXT); 149 registerParameterDataType(ICalParameters.LANGUAGE, ICalDataType.TEXT); 150 registerParameterDataType(ICalParameters.MEMBER, ICalDataType.CAL_ADDRESS); 151 registerParameterDataType(ICalParameters.PARTSTAT, ICalDataType.TEXT); 152 registerParameterDataType(ICalParameters.RANGE, ICalDataType.TEXT); 153 registerParameterDataType(ICalParameters.RELATED, ICalDataType.TEXT); 154 registerParameterDataType(ICalParameters.RELTYPE, ICalDataType.TEXT); 155 registerParameterDataType(ICalParameters.ROLE, ICalDataType.TEXT); 156 registerParameterDataType(ICalParameters.RSVP, ICalDataType.BOOLEAN); 157 registerParameterDataType(ICalParameters.SENT_BY, ICalDataType.CAL_ADDRESS); 158 registerParameterDataType(ICalParameters.TZID, ICalDataType.TEXT); 159 } 160 161 private final Writer writer; 162 private final ICalVersion targetVersion = ICalVersion.V2_0; 163 private final TransformerHandler handler; 164 private final boolean icalendarElementExists; 165 private String indent; 166 private int level = 0; 167 private boolean textNodeJustPrinted = false, started = false; 168 169 /** 170 * @param out the output stream to write to (UTF-8 encoding will be used) 171 */ 172 public XCalWriter(OutputStream out) { 173 this(utf8Writer(out)); 174 } 175 176 /** 177 * @param file the file to write to (UTF-8 encoding will be used). 178 * @throws IOException if there's a problem opening the file 179 */ 180 public XCalWriter(File file) throws IOException { 181 this(utf8Writer(file)); 182 } 183 184 /** 185 * @param writer the writer to write to 186 */ 187 public XCalWriter(Writer writer) { 188 this(writer, null); 189 } 190 191 /** 192 * @param parent the DOM node to add child elements to 193 */ 194 public XCalWriter(Node parent) { 195 this(null, parent); 196 } 197 198 private XCalWriter(Writer writer, Node parent) { 199 this.writer = writer; 200 201 if (parent instanceof Document) { 202 Node root = parent.getFirstChild(); 203 if (root != null) { 204 parent = root; 205 } 206 } 207 this.icalendarElementExists = isICalendarElement(parent); 208 209 try { 210 SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance(); 211 handler = factory.newTransformerHandler(); 212 } catch (TransformerConfigurationException e) { 213 throw new RuntimeException(e); 214 } 215 216 Result result = (writer == null) ? new DOMResult(parent) : new StreamResult(writer); 217 handler.setResult(result); 218 } 219 220 /** 221 * Set the indentation string to use for pretty-printing the output. 222 * @param indent the indentation string (e.g. 2 spaces) or null to disable 223 * pretty-printing (defaults to null) 224 */ 225 public void setIndent(String indent) { 226 this.indent = indent; 227 } 228 229 private boolean isICalendarElement(Node node) { 230 if (node == null) { 231 return false; 232 } 233 234 if (!(node instanceof Element)) { 235 return false; 236 } 237 238 return XmlUtils.hasQName(node, ICALENDAR); 239 } 240 241 /** 242 * Registers the data type of an experimental parameter. Experimental 243 * parameters use the "unknown" data type by default. 244 * @param parameterName the parameter name (e.g. "x-foo") 245 * @param dataType the data type or null to remove 246 */ 247 public void registerParameterDataType(String parameterName, ICalDataType dataType) { 248 parameterName = parameterName.toLowerCase(); 249 if (dataType == null) { 250 parameterDataTypes.remove(parameterName); 251 } else { 252 parameterDataTypes.put(parameterName, dataType); 253 } 254 } 255 256 @Override 257 protected void _write(ICalendar ical) throws IOException { 258 try { 259 if (!started) { 260 handler.startDocument(); 261 262 if (!icalendarElementExists) { 263 //don't output a <icalendar> element if the parent is a <icalendar> element 264 start(ICALENDAR); 265 level++; 266 } 267 268 started = true; 269 } 270 271 write((ICalComponent) ical); 272 } catch (SAXException e) { 273 throw new IOException(e); 274 } 275 } 276 277 @Override 278 protected ICalVersion getTargetVersion() { 279 return targetVersion; 280 } 281 282 @SuppressWarnings({ "rawtypes", "unchecked" }) 283 private void write(ICalComponent component) throws SAXException { 284 ICalComponentScribe scribe = index.getComponentScribe(component); 285 String name = scribe.getComponentName().toLowerCase(); 286 287 start(name); 288 level++; 289 290 List properties = scribe.getProperties(component); 291 if (component instanceof ICalendar && component.getProperty(Version.class) == null) { 292 properties.add(0, new Version(targetVersion)); 293 } 294 295 if (!properties.isEmpty()) { 296 start(PROPERTIES); 297 level++; 298 299 for (Object propertyObj : properties) { 300 context.setParent(component); //set parent here incase a scribe resets the parent 301 ICalProperty property = (ICalProperty) propertyObj; 302 write(property); 303 } 304 305 level--; 306 end(PROPERTIES); 307 } 308 309 Collection subComponents = scribe.getComponents(component); 310 if (component instanceof ICalendar) { 311 //add the VTIMEZONE components that were auto-generated by TimezoneOptions 312 Collection<VTimezone> tzs = tzinfo.getComponents(); 313 for (VTimezone tz : tzs) { 314 if (!subComponents.contains(tz)) { 315 subComponents.add(tz); 316 } 317 } 318 } 319 if (!subComponents.isEmpty()) { 320 start(COMPONENTS); 321 level++; 322 323 for (Object subComponentObj : subComponents) { 324 ICalComponent subComponent = (ICalComponent) subComponentObj; 325 write(subComponent); 326 } 327 328 level--; 329 end(COMPONENTS); 330 } 331 332 level--; 333 end(name); 334 } 335 336 @SuppressWarnings({ "rawtypes", "unchecked" }) 337 private void write(ICalProperty property) throws SAXException { 338 ICalPropertyScribe scribe = index.getPropertyScribe(property); 339 ICalParameters parameters = scribe.prepareParameters(property, context); 340 341 //get the property element to write 342 Element propertyElement; 343 if (property instanceof Xml) { 344 Xml xml = (Xml) property; 345 Document value = xml.getValue(); 346 if (value == null) { 347 return; 348 } 349 propertyElement = XmlUtils.getRootElement(value); 350 } else { 351 QName qname = scribe.getQName(); 352 propertyElement = DOC.createElementNS(qname.getNamespaceURI(), qname.getLocalPart()); 353 try { 354 scribe.writeXml(property, propertyElement, context); 355 } catch (SkipMeException e) { 356 return; 357 } 358 } 359 360 start(propertyElement); 361 level++; 362 363 write(parameters); 364 write(propertyElement); 365 366 level--; 367 end(propertyElement); 368 } 369 370 private void write(Element propertyElement) throws SAXException { 371 NodeList children = propertyElement.getChildNodes(); 372 for (int i = 0; i < children.getLength(); i++) { 373 Node child = children.item(i); 374 375 if (child instanceof Element) { 376 Element element = (Element) child; 377 378 if (element.hasChildNodes()) { 379 start(element); 380 level++; 381 382 write(element); 383 384 level--; 385 end(element); 386 } else { 387 childless(element); 388 } 389 390 continue; 391 } 392 393 if (child instanceof Text) { 394 Text text = (Text) child; 395 text(text.getTextContent()); 396 continue; 397 } 398 } 399 } 400 401 private void write(ICalParameters parameters) throws SAXException { 402 if (parameters.isEmpty()) { 403 return; 404 } 405 406 start(PARAMETERS); 407 level++; 408 409 for (Map.Entry<String, List<String>> parameter : parameters) { 410 String parameterName = parameter.getKey().toLowerCase(); 411 start(parameterName); 412 level++; 413 414 for (String parameterValue : parameter.getValue()) { 415 ICalDataType dataType = parameterDataTypes.get(parameterName); 416 String dataTypeElementName = (dataType == null) ? "unknown" : dataType.getName().toLowerCase(); 417 418 start(dataTypeElementName); 419 text(parameterValue); 420 end(dataTypeElementName); 421 } 422 423 level--; 424 end(parameterName); 425 } 426 427 level--; 428 end(PARAMETERS); 429 } 430 431 private void indent() throws SAXException { 432 if (indent == null) { 433 return; 434 } 435 436 /* 437 * "\n" is hard-coded here because if the Windows "\r\n" is used, it 438 * will encode the "\r" character for XML (" ") 439 */ 440 String str = '\n' + StringUtils.repeat(indent, level); 441 handler.ignorableWhitespace(str.toCharArray(), 0, str.length()); 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 indent(); 453 handler.startElement(element.getNamespaceURI(), "", element.getLocalName(), attributes); 454 handler.endElement(element.getNamespaceURI(), "", element.getLocalName()); 455 } 456 457 private void start(Element element) throws SAXException { 458 Attributes attributes = getElementAttributes(element); 459 start(element.getNamespaceURI(), element.getLocalName(), attributes); 460 } 461 462 private void start(String element) throws SAXException { 463 start(element, null); 464 } 465 466 private void start(QName qname) throws SAXException { 467 start(qname, null); 468 } 469 470 private void start(QName qname, Attributes attributes) throws SAXException { 471 start(qname.getNamespaceURI(), qname.getLocalPart(), attributes); 472 } 473 474 private void start(String element, Attributes attributes) throws SAXException { 475 start(XCAL_NS, element, attributes); 476 } 477 478 private void start(String namespace, String element, Attributes attributes) throws SAXException { 479 indent(); 480 handler.startElement(namespace, "", element, attributes); 481 } 482 483 private void end(Element element) throws SAXException { 484 end(element.getNamespaceURI(), element.getLocalName()); 485 } 486 487 private void end(String element) throws SAXException { 488 end(XCAL_NS, element); 489 } 490 491 private void end(QName qname) throws SAXException { 492 end(qname.getNamespaceURI(), qname.getLocalPart()); 493 } 494 495 private void end(String namespace, String element) throws SAXException { 496 if (!textNodeJustPrinted) { 497 indent(); 498 } 499 500 handler.endElement(namespace, "", element); 501 textNodeJustPrinted = false; 502 } 503 504 private void text(String text) throws SAXException { 505 handler.characters(text.toCharArray(), 0, text.length()); 506 textNodeJustPrinted = true; 507 } 508 509 private Attributes getElementAttributes(Element element) { 510 AttributesImpl attributes = new AttributesImpl(); 511 NamedNodeMap attributeNodes = element.getAttributes(); 512 for (int i = 0; i < attributeNodes.getLength(); i++) { 513 Node node = attributeNodes.item(i); 514 attributes.addAttribute(node.getNamespaceURI(), "", node.getLocalName(), "", node.getNodeValue()); 515 } 516 return attributes; 517 } 518 519 /** 520 * Terminates the XML document and closes the output stream. 521 */ 522 public void close() throws IOException { 523 try { 524 if (!started) { 525 handler.startDocument(); 526 527 if (!icalendarElementExists) { 528 //don't output a <icalendar> element if the parent is a <icalendar> element 529 start(ICALENDAR); 530 level++; 531 } 532 } 533 534 if (!icalendarElementExists) { 535 level--; 536 end(ICALENDAR); 537 } 538 handler.endDocument(); 539 } catch (SAXException e) { 540 throw new IOException(e); 541 } 542 543 if (writer != null) { 544 writer.close(); 545 } 546 } 547}