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.io.xml.XCalQNames.VCALENDAR; 009import static biweekly.util.IOUtils.utf8Writer; 010 011import java.io.File; 012import java.io.FileInputStream; 013import java.io.IOException; 014import java.io.InputStream; 015import java.io.OutputStream; 016import java.io.Reader; 017import java.io.StringWriter; 018import java.io.Writer; 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.Iterator; 024import java.util.List; 025import java.util.Map; 026 027import javax.xml.namespace.QName; 028import javax.xml.transform.OutputKeys; 029import javax.xml.transform.TransformerException; 030import javax.xml.xpath.XPath; 031import javax.xml.xpath.XPathConstants; 032import javax.xml.xpath.XPathExpressionException; 033import javax.xml.xpath.XPathFactory; 034 035import org.w3c.dom.Document; 036import org.w3c.dom.Element; 037import org.xml.sax.SAXException; 038 039import biweekly.ICalDataType; 040import biweekly.ICalVersion; 041import biweekly.ICalendar; 042import biweekly.Warning; 043import biweekly.component.ICalComponent; 044import biweekly.component.VTimezone; 045import biweekly.io.CannotParseException; 046import biweekly.io.SkipMeException; 047import biweekly.io.StreamReader; 048import biweekly.io.StreamWriter; 049import biweekly.io.scribe.ScribeIndex; 050import biweekly.io.scribe.component.ICalComponentScribe; 051import biweekly.io.scribe.component.ICalendarScribe; 052import biweekly.io.scribe.property.ICalPropertyScribe; 053import biweekly.parameter.ICalParameters; 054import biweekly.property.ICalProperty; 055import biweekly.property.Version; 056import biweekly.property.Xml; 057import biweekly.util.IOUtils; 058import biweekly.util.XmlUtils; 059 060/* 061 Copyright (c) 2013-2015, Michael Angstadt 062 All rights reserved. 063 064 Redistribution and use in source and binary forms, with or without 065 modification, are permitted provided that the following conditions are met: 066 067 1. Redistributions of source code must retain the above copyright notice, this 068 list of conditions and the following disclaimer. 069 2. Redistributions in binary form must reproduce the above copyright notice, 070 this list of conditions and the following disclaimer in the documentation 071 and/or other materials provided with the distribution. 072 073 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 074 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 075 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 076 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 077 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 078 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 079 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 080 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 081 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 082 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 083 */ 084 085//@formatter:off 086/** 087 * <p> 088 * Represents an XML document that contains iCalendar objects ("xCal" standard). 089 * This class can be used to read and write xCal documents. 090 * </p> 091 * <p> 092 * <b>Examples:</b> 093 * 094 * <pre class="brush:java"> 095 * String xml = 096 * "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" + 097 * "<icalendar xmlns=\"urn:ietf:params:xml:ns:icalendar-2.0\">" + 098 * "<vcalendar>" + 099 * "<properties>" + 100 * "<prodid><text>-//Example Inc.//Example Client//EN</text></prodid>" + 101 * "<version><text>2.0</text></version>" + 102 * "</properties>" + 103 * "<components>" + 104 * "<vevent>" + 105 * "<properties>" + 106 * "<dtstart><date-time>2013-06-27T13:00:00Z</date-time></dtstart>" + 107 * "<dtend><date-time>2013-06-27T15:00:00Z</date-time></dtend>" + 108 * "<summary><text>Team Meeting</text></summary>" + 109 * "</properties>" + 110 * "</vevent>" + 111 * "</components>" + 112 * "</vcalendar>" + 113 * "</icalendar>"; 114 * 115 * //parsing an existing xCal document 116 * XCalDocument xcal = new XCalDocument(xml); 117 * List<ICalendar> icals = xcal.parseAll(); 118 * 119 * //creating an empty xCal document 120 * XCalDocument xcal = new XCalDocument(); 121 * 122 * //ICalendar objects can be added at any time 123 * ICalendar ical = new ICalendar(); 124 * xcal.add(ical); 125 * 126 * //retrieving the raw XML DOM 127 * Document document = xcal.getDocument(); 128 * 129 * //call one of the "write()" methods to output the xCal document 130 * File file = new File("meeting.xml"); 131 * xcal.write(file); 132 * </pre> 133 * 134 * </p> 135 * @author Michael Angstadt 136 * @see <a href="http://tools.ietf.org/html/rfc6321">RFC 6321</a> 137 */ 138//@formatter:on 139public class XCalDocument { 140 private static final ICalendarScribe icalMarshaller = ScribeIndex.getICalendarScribe(); 141 private static final XCalNamespaceContext nsContext = new XCalNamespaceContext("xcal"); 142 143 private final Document document; 144 private final ICalVersion targetVersion = ICalVersion.V2_0; 145 private Element root; 146 147 /** 148 * Parses an xCal document from a string. 149 * @param xml the xCal document in the form of a string 150 * @throws SAXException if there's a problem parsing the XML 151 */ 152 public XCalDocument(String xml) throws SAXException { 153 this(XmlUtils.toDocument(xml)); 154 } 155 156 /** 157 * Parses an xCal document from an input stream. 158 * @param in the input stream to read the the xCal document from 159 * @throws IOException if there's a problem reading from the input stream 160 * @throws SAXException if there's a problem parsing the XML 161 */ 162 public XCalDocument(InputStream in) throws SAXException, IOException { 163 this(XmlUtils.toDocument(in)); 164 } 165 166 /** 167 * Parses an xCal document from a file. 168 * @param file the file containing the xCal document 169 * @throws IOException if there's a problem reading from the file 170 * @throws SAXException if there's a problem parsing the XML 171 */ 172 public XCalDocument(File file) throws SAXException, IOException { 173 this(readFile(file)); 174 } 175 176 private static Document readFile(File file) throws SAXException, IOException { 177 InputStream in = new FileInputStream(file); 178 try { 179 return XmlUtils.toDocument(in); 180 } finally { 181 IOUtils.closeQuietly(in); 182 } 183 } 184 185 /** 186 * <p> 187 * Parses an xCal document from a reader. 188 * </p> 189 * <p> 190 * Note that use of this constructor is discouraged. It ignores the 191 * character encoding that is defined within the XML document itself, and 192 * should only be used if the encoding is undefined or if the encoding needs 193 * to be ignored for whatever reason. The {@link #XCalDocument(InputStream)} 194 * constructor should be used instead, since it takes the XML document's 195 * character encoding into account when parsing. 196 * </p> 197 * @param reader the reader to read the xCal document from 198 * @throws IOException if there's a problem reading from the reader 199 * @throws SAXException if there's a problem parsing the XML 200 */ 201 public XCalDocument(Reader reader) throws SAXException, IOException { 202 this(XmlUtils.toDocument(reader)); 203 } 204 205 /** 206 * Wraps an existing XML DOM object. 207 * @param document the XML DOM that contains the xCal document 208 */ 209 public XCalDocument(Document document) { 210 this.document = document; 211 212 XPath xpath = XPathFactory.newInstance().newXPath(); 213 xpath.setNamespaceContext(nsContext); 214 215 try { 216 //find the <icalendar> element 217 String prefix = nsContext.getPrefix(); 218 root = (Element) xpath.evaluate("//" + prefix + ":" + ICALENDAR.getLocalPart(), document, XPathConstants.NODE); 219 } catch (XPathExpressionException e) { 220 //never thrown, xpath expression is hard coded 221 } 222 } 223 224 /** 225 * Creates an empty xCal document. 226 */ 227 public XCalDocument() { 228 this(createRoot()); 229 } 230 231 private static Document createRoot() { 232 Document document = XmlUtils.createDocument(); 233 Element root = document.createElementNS(ICALENDAR.getNamespaceURI(), ICALENDAR.getLocalPart()); 234 document.appendChild(root); 235 return document; 236 } 237 238 /** 239 * Gets the raw XML DOM object. 240 * @return the XML DOM 241 */ 242 public Document getDocument() { 243 return document; 244 } 245 246 /** 247 * Parses all iCalendar objects from this XML document. 248 * @return the parsed iCalendar objects 249 */ 250 public List<ICalendar> getICalendars() { 251 try { 252 return reader().readAll(); 253 } catch (IOException e) { 254 //not thrown because reading from DOM 255 throw new RuntimeException(e); 256 } 257 } 258 259 /** 260 * Adds an iCalendar object to this XML document. 261 * @param ical the iCalendar object to add 262 */ 263 public void add(ICalendar ical) { 264 writer().write(ical); 265 } 266 267 /** 268 * Creates a {@link StreamReader} object that parses iCalendar objects from 269 * this XML document. 270 * @return the reader 271 */ 272 public StreamReader reader() { 273 return new XCalDocumentStreamReader(); 274 } 275 276 /** 277 * Creates a {@link StreamWriter} object that adds iCalendar objects to this 278 * XML document. 279 * @return the writer 280 */ 281 public XCalDocumentStreamWriter writer() { 282 return new XCalDocumentStreamWriter(); 283 } 284 285 /** 286 * Writes the xCal document to a string without pretty-printing it. 287 * @return the XML string 288 */ 289 public String write() { 290 return write(-1); 291 } 292 293 /** 294 * Writes the xCal document to a string and pretty-prints it. 295 * @param indent the number of indent spaces to use for pretty-printing 296 * @return the XML string 297 */ 298 public String write(int indent) { 299 StringWriter sw = new StringWriter(); 300 try { 301 write(sw, indent); 302 } catch (TransformerException e) { 303 //writing to string 304 } 305 return sw.toString(); 306 } 307 308 /** 309 * Writes the xCal document to an output stream without pretty-printing it. 310 * @param out the output stream 311 * @throws TransformerException if there's a problem writing to the output 312 * stream 313 */ 314 public void write(OutputStream out) throws TransformerException { 315 write(out, -1); 316 } 317 318 /** 319 * Writes the xCal document to an output stream and pretty-prints it. 320 * @param out the output stream 321 * @param indent the number of indent spaces to use for pretty-printing 322 * @throws TransformerException if there's a problem writing to the output 323 * stream 324 */ 325 public void write(OutputStream out, int indent) throws TransformerException { 326 write(utf8Writer(out), indent); 327 } 328 329 /** 330 * Writes the xCal document to a file without pretty-printing it. 331 * @param file the file 332 * @throws IOException if there's a problem writing to the file 333 * @throws TransformerException if there's a problem writing the XML 334 */ 335 public void write(File file) throws TransformerException, IOException { 336 write(file, -1); 337 } 338 339 /** 340 * Writes the xCal document to a file and pretty-prints it. 341 * @param file the file stream 342 * @param indent the number of indent spaces to use for pretty-printing 343 * @throws IOException if there's a problem writing to the file 344 * @throws TransformerException if there's a problem writing the XML 345 */ 346 public void write(File file, int indent) throws TransformerException, IOException { 347 Writer writer = utf8Writer(file); 348 try { 349 write(writer, indent); 350 } finally { 351 IOUtils.closeQuietly(writer); 352 } 353 } 354 355 /** 356 * Writes the xCal document to a writer without pretty-printing it. 357 * @param writer the writer 358 * @throws TransformerException if there's a problem writing to the writer 359 */ 360 public void write(Writer writer) throws TransformerException { 361 write(writer, -1); 362 } 363 364 /** 365 * Writes the xCal document to a writer and pretty-prints it. 366 * @param writer the writer 367 * @param indent the number of indent spaces to use for pretty-printing 368 * @throws TransformerException if there's a problem writing to the writer 369 */ 370 public void write(Writer writer, int indent) throws TransformerException { 371 Map<String, String> properties = new HashMap<String, String>(); 372 if (indent >= 0) { 373 properties.put(OutputKeys.INDENT, "yes"); 374 properties.put("{http://xml.apache.org/xslt}indent-amount", indent + ""); 375 } 376 XmlUtils.toWriter(document, writer, properties); 377 } 378 379 @Override 380 public String toString() { 381 return write(2); 382 } 383 384 private class XCalDocumentStreamReader extends StreamReader { 385 private final Iterator<Element> vcalendarElements = getVCalendarElements().iterator(); 386 387 @Override 388 protected ICalendar _readNext() throws IOException { 389 if (!vcalendarElements.hasNext()) { 390 return null; 391 } 392 393 context.setVersion(ICalVersion.V2_0); 394 Element vcalendarElement = vcalendarElements.next(); 395 return parseICal(vcalendarElement); 396 } 397 398 private ICalendar parseICal(Element icalElement) { 399 ICalComponent root = parseComponent(icalElement); 400 if (root instanceof ICalendar) { 401 return (ICalendar) root; 402 } 403 404 //shouldn't happen, since only <vcalendar> elements are passed into this method 405 ICalendar ical = icalMarshaller.emptyInstance(); 406 ical.addComponent(root); 407 return ical; 408 } 409 410 private ICalComponent parseComponent(Element componentElement) { 411 //create the component object 412 ICalComponentScribe<? extends ICalComponent> scribe = index.getComponentScribe(componentElement.getLocalName(), ICalVersion.V2_0); 413 ICalComponent component = scribe.emptyInstance(); 414 boolean isICalendar = component instanceof ICalendar; 415 416 //parse properties 417 for (Element propertyWrapperElement : getChildElements(componentElement, PROPERTIES)) { //there should be only one <properties> element, but parse them all incase there are more 418 for (Element propertyElement : XmlUtils.toElementList(propertyWrapperElement.getChildNodes())) { 419 ICalProperty property = parseProperty(propertyElement); 420 if (property == null) { 421 continue; 422 } 423 424 //set "ICalendar.version" if the value of the VERSION property is recognized 425 //otherwise, unmarshal VERSION like a normal property 426 if (isICalendar && property instanceof Version) { 427 Version version = (Version) property; 428 ICalVersion icalVersion = version.toICalVersion(); 429 if (icalVersion != null) { 430 context.setVersion(icalVersion); 431 continue; 432 } 433 } 434 435 component.addProperty(property); 436 } 437 } 438 439 //parse sub-components 440 for (Element componentWrapperElement : getChildElements(componentElement, COMPONENTS)) { //there should be only one <components> element, but parse them all incase there are more 441 for (Element subComponentElement : XmlUtils.toElementList(componentWrapperElement.getChildNodes())) { 442 if (!XCAL_NS.equals(subComponentElement.getNamespaceURI())) { 443 continue; 444 } 445 446 ICalComponent subComponent = parseComponent(subComponentElement); 447 component.addComponent(subComponent); 448 } 449 } 450 451 return component; 452 } 453 454 private ICalProperty parseProperty(Element propertyElement) { 455 ICalParameters parameters = parseParameters(propertyElement); 456 String propertyName = propertyElement.getLocalName(); 457 QName qname = new QName(propertyElement.getNamespaceURI(), propertyName); 458 459 context.getWarnings().clear(); 460 ICalPropertyScribe<? extends ICalProperty> scribe = index.getPropertyScribe(qname); 461 try { 462 ICalProperty property = scribe.parseXml(propertyElement, parameters, context); 463 for (Warning warning : context.getWarnings()) { 464 warnings.add(null, propertyName, warning); 465 } 466 return property; 467 } catch (SkipMeException e) { 468 warnings.add(null, propertyName, 0, e.getMessage()); 469 return null; 470 } catch (CannotParseException e) { 471 warnings.add(null, propertyName, 16, e.getMessage()); 472 473 scribe = index.getPropertyScribe(Xml.class); 474 return scribe.parseXml(propertyElement, parameters, context); 475 } 476 } 477 478 private ICalParameters parseParameters(Element propertyElement) { 479 ICalParameters parameters = new ICalParameters(); 480 481 for (Element parametersElement : getChildElements(propertyElement, PARAMETERS)) { //there should be only one <parameters> element, but parse them all incase there are more 482 List<Element> paramElements = XmlUtils.toElementList(parametersElement.getChildNodes()); 483 for (Element paramElement : paramElements) { 484 if (!XCAL_NS.equals(paramElement.getNamespaceURI())) { 485 continue; 486 } 487 488 String name = paramElement.getLocalName().toUpperCase(); 489 List<Element> valueElements = XmlUtils.toElementList(paramElement.getChildNodes()); 490 if (valueElements.isEmpty()) { 491 //this should never be true if the xCal follows the specs 492 String value = paramElement.getTextContent(); 493 parameters.put(name, value); 494 continue; 495 } 496 497 for (Element valueElement : valueElements) { 498 if (!XCAL_NS.equals(valueElement.getNamespaceURI())) { 499 continue; 500 } 501 502 String value = valueElement.getTextContent(); 503 parameters.put(name, value); 504 } 505 } 506 } 507 508 return parameters; 509 } 510 511 private List<Element> getVCalendarElements() { 512 return (root == null) ? Collections.<Element> emptyList() : getChildElements(root, VCALENDAR); 513 } 514 515 private List<Element> getChildElements(Element parent, QName qname) { 516 List<Element> elements = new ArrayList<Element>(); 517 for (Element child : XmlUtils.toElementList(parent.getChildNodes())) { 518 QName childQName = new QName(child.getNamespaceURI(), child.getLocalName()); 519 if (qname.equals(childQName)) { 520 elements.add(child); 521 } 522 } 523 return elements; 524 } 525 526 public void close() { 527 //do nothing 528 } 529 } 530 531 public class XCalDocumentStreamWriter extends StreamWriter { 532 /** 533 * Defines the names of the XML elements that are used to hold each 534 * parameter's value. 535 */ 536 private final Map<String, ICalDataType> parameterDataTypes = new HashMap<String, ICalDataType>(); 537 { 538 registerParameterDataType(ICalParameters.CN, ICalDataType.TEXT); 539 registerParameterDataType(ICalParameters.ALTREP, ICalDataType.URI); 540 registerParameterDataType(ICalParameters.CUTYPE, ICalDataType.TEXT); 541 registerParameterDataType(ICalParameters.DELEGATED_FROM, ICalDataType.CAL_ADDRESS); 542 registerParameterDataType(ICalParameters.DELEGATED_TO, ICalDataType.CAL_ADDRESS); 543 registerParameterDataType(ICalParameters.DIR, ICalDataType.URI); 544 registerParameterDataType(ICalParameters.ENCODING, ICalDataType.TEXT); 545 registerParameterDataType(ICalParameters.FMTTYPE, ICalDataType.TEXT); 546 registerParameterDataType(ICalParameters.FBTYPE, ICalDataType.TEXT); 547 registerParameterDataType(ICalParameters.LANGUAGE, ICalDataType.TEXT); 548 registerParameterDataType(ICalParameters.MEMBER, ICalDataType.CAL_ADDRESS); 549 registerParameterDataType(ICalParameters.PARTSTAT, ICalDataType.TEXT); 550 registerParameterDataType(ICalParameters.RANGE, ICalDataType.TEXT); 551 registerParameterDataType(ICalParameters.RELATED, ICalDataType.TEXT); 552 registerParameterDataType(ICalParameters.RELTYPE, ICalDataType.TEXT); 553 registerParameterDataType(ICalParameters.ROLE, ICalDataType.TEXT); 554 registerParameterDataType(ICalParameters.RSVP, ICalDataType.BOOLEAN); 555 registerParameterDataType(ICalParameters.SENT_BY, ICalDataType.CAL_ADDRESS); 556 registerParameterDataType(ICalParameters.TZID, ICalDataType.TEXT); 557 } 558 559 @Override 560 public void write(ICalendar ical) { 561 try { 562 super.write(ical); 563 } catch (IOException e) { 564 //won't be thrown because we're writing to DOM 565 } 566 } 567 568 @Override 569 protected void _write(ICalendar ical) { 570 Element element = buildComponentElement(ical); 571 572 if (root == null) { 573 root = buildElement(ICALENDAR); 574 Element documentRoot = XmlUtils.getRootElement(document); 575 if (documentRoot == null) { 576 document.appendChild(root); 577 } else { 578 documentRoot.appendChild(root); 579 } 580 } 581 root.appendChild(element); 582 } 583 584 /** 585 * Registers the data type of an experimental parameter. Experimental 586 * parameters use the "unknown" xCal data type by default. 587 * @param parameterName the parameter name (e.g. "x-foo") 588 * @param dataType the data type or null to remove 589 */ 590 public void registerParameterDataType(String parameterName, ICalDataType dataType) { 591 parameterName = parameterName.toLowerCase(); 592 if (dataType == null) { 593 parameterDataTypes.remove(parameterName); 594 } else { 595 parameterDataTypes.put(parameterName, dataType); 596 } 597 } 598 599 @SuppressWarnings({ "rawtypes", "unchecked" }) 600 private Element buildComponentElement(ICalComponent component) { 601 ICalComponentScribe componentScribe = index.getComponentScribe(component); 602 Element componentElement = buildElement(componentScribe.getComponentName().toLowerCase()); 603 604 Element propertiesWrapperElement = buildElement(PROPERTIES); 605 List propertyObjs = componentScribe.getProperties(component); 606 if (component instanceof ICalendar && component.getProperty(Version.class) == null) { 607 //add a version property 608 propertyObjs.add(0, new Version(targetVersion)); 609 } 610 611 for (Object propertyObj : propertyObjs) { 612 context.setParent(component); //set parent here incase a scribe resets the parent 613 ICalProperty property = (ICalProperty) propertyObj; 614 615 //create property element 616 Element propertyElement = buildPropertyElement(property); 617 if (propertyElement != null) { 618 propertiesWrapperElement.appendChild(propertyElement); 619 } 620 } 621 if (propertiesWrapperElement.hasChildNodes()) { 622 componentElement.appendChild(propertiesWrapperElement); 623 } 624 625 Collection subComponents = componentScribe.getComponents(component); 626 if (component instanceof ICalendar) { 627 //add the VTIMEZONE components that were auto-generated by TimezoneOptions 628 Collection<VTimezone> tzs = tzinfo.getComponents(); 629 for (VTimezone tz : tzs) { 630 if (!subComponents.contains(tz)) { 631 subComponents.add(tz); 632 } 633 } 634 } 635 Element componentsWrapperElement = buildElement(COMPONENTS); 636 for (Object subComponentObj : subComponents) { 637 ICalComponent subComponent = (ICalComponent) subComponentObj; 638 Element subComponentElement = buildComponentElement(subComponent); 639 if (subComponentElement != null) { 640 componentsWrapperElement.appendChild(subComponentElement); 641 } 642 } 643 if (componentsWrapperElement.hasChildNodes()) { 644 componentElement.appendChild(componentsWrapperElement); 645 } 646 647 return componentElement; 648 } 649 650 @SuppressWarnings({ "rawtypes", "unchecked" }) 651 private Element buildPropertyElement(ICalProperty property) { 652 Element propertyElement; 653 ICalParameters parameters; 654 655 if (property instanceof Xml) { 656 Xml xml = (Xml) property; 657 658 Document value = xml.getValue(); 659 if (value == null) { 660 return null; 661 } 662 663 //import the XML element into the xCal DOM 664 propertyElement = XmlUtils.getRootElement(value); 665 propertyElement = (Element) document.importNode(propertyElement, true); 666 667 //get parameters 668 parameters = property.getParameters(); 669 } else { 670 ICalPropertyScribe propertyScribe = index.getPropertyScribe(property); 671 propertyElement = buildElement(propertyScribe.getQName()); 672 673 //marshal value 674 try { 675 propertyScribe.writeXml(property, propertyElement, context); 676 } catch (SkipMeException e) { 677 return null; 678 } 679 680 //get parameters 681 parameters = propertyScribe.prepareParameters(property, context); 682 } 683 684 //build parameters 685 Element parametersWrapperElement = buildParametersElement(parameters); 686 if (parametersWrapperElement.hasChildNodes()) { 687 propertyElement.insertBefore(parametersWrapperElement, propertyElement.getFirstChild()); 688 } 689 690 return propertyElement; 691 } 692 693 private Element buildParametersElement(ICalParameters parameters) { 694 Element parametersWrapperElement = buildElement(PARAMETERS); 695 696 for (Map.Entry<String, List<String>> parameter : parameters) { 697 String name = parameter.getKey().toLowerCase(); 698 ICalDataType dataType = parameterDataTypes.get(name); 699 String dataTypeStr = (dataType == null) ? "unknown" : dataType.getName().toLowerCase(); 700 701 Element parameterElement = buildAndAppendElement(name, parametersWrapperElement); 702 for (String parameterValue : parameter.getValue()) { 703 Element parameterValueElement = buildAndAppendElement(dataTypeStr, parameterElement); 704 parameterValueElement.setTextContent(parameterValue); 705 } 706 } 707 708 return parametersWrapperElement; 709 } 710 711 private Element buildElement(String localName) { 712 return buildElement(new QName(XCAL_NS, localName)); 713 } 714 715 private Element buildElement(QName qname) { 716 return document.createElementNS(qname.getNamespaceURI(), qname.getLocalPart()); 717 } 718 719 private Element buildAndAppendElement(String localName, Element parent) { 720 return buildAndAppendElement(new QName(XCAL_NS, localName), parent); 721 } 722 723 private Element buildAndAppendElement(QName qname, Element parent) { 724 Element child = document.createElementNS(qname.getNamespaceURI(), qname.getLocalPart()); 725 parent.appendChild(child); 726 return child; 727 } 728 729 @Override 730 protected ICalVersion getTargetVersion() { 731 return ICalVersion.V2_0; 732 } 733 734 public void close() { 735 //do nothing 736 } 737 } 738}