001 package biweekly.io.xml; 002 003 import static biweekly.io.xml.XCalNamespaceContext.XCAL_NS; 004 import static biweekly.util.IOUtils.utf8Writer; 005 006 import java.io.File; 007 import java.io.FileInputStream; 008 import java.io.IOException; 009 import java.io.InputStream; 010 import java.io.OutputStream; 011 import java.io.Reader; 012 import java.io.StringWriter; 013 import java.io.Writer; 014 import java.util.ArrayList; 015 import java.util.Collections; 016 import java.util.HashMap; 017 import java.util.List; 018 import java.util.Map; 019 020 import javax.xml.namespace.QName; 021 import javax.xml.transform.OutputKeys; 022 import javax.xml.transform.TransformerException; 023 import javax.xml.xpath.XPath; 024 import javax.xml.xpath.XPathConstants; 025 import javax.xml.xpath.XPathExpressionException; 026 import javax.xml.xpath.XPathFactory; 027 028 import org.w3c.dom.Document; 029 import org.w3c.dom.Element; 030 import org.xml.sax.SAXException; 031 032 import biweekly.ICalDataType; 033 import biweekly.ICalendar; 034 import biweekly.component.ICalComponent; 035 import biweekly.component.marshaller.ICalComponentMarshaller; 036 import biweekly.component.marshaller.ICalendarMarshaller; 037 import biweekly.io.CannotParseException; 038 import biweekly.io.ICalMarshallerRegistrar; 039 import biweekly.io.SkipMeException; 040 import biweekly.parameter.ICalParameters; 041 import biweekly.property.ICalProperty; 042 import biweekly.property.Xml; 043 import biweekly.property.marshaller.ICalPropertyMarshaller; 044 import biweekly.property.marshaller.ICalPropertyMarshaller.Result; 045 import biweekly.util.IOUtils; 046 import biweekly.util.XmlUtils; 047 048 /* 049 Copyright (c) 2013, 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 073 //@formatter:off 074 /** 075 * <p> 076 * Represents an XML document that contains iCalendar objects ("xCal" standard). 077 * This class can be used to read and write xCal documents. 078 * </p> 079 * <p> 080 * <b>Examples:</b> 081 * 082 * <pre class="brush:java"> 083 * String xml = 084 * "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" + 085 * "<icalendar xmlns=\"urn:ietf:params:xml:ns:icalendar-2.0\">" + 086 * "<vcalendar>" + 087 * "<properties>" + 088 * "<prodid><text>-//Example Inc.//Example Client//EN</text></prodid>" + 089 * "<version><text>2.0</text></version>" + 090 * "</properties>" + 091 * "<components>" + 092 * "<vevent>" + 093 * "<properties>" + 094 * "<dtstart><date-time>2013-06-27T13:00:00Z</date-time></dtstart>" + 095 * "<dtend><date-time>2013-06-27T15:00:00Z</date-time></dtend>" + 096 * "<summary><text>Team Meeting</text></summary>" + 097 * "</properties>" + 098 * "</vevent>" + 099 * "</components>" + 100 * "</vcalendar>" + 101 * "</icalendar>"; 102 * 103 * //parsing an existing xCal document 104 * XCalDocument xcal = new XCalDocument(xml); 105 * List<ICalendar> icals = xcal.parseAll(); 106 * 107 * //creating an empty xCal document 108 * XCalDocument xcal = new XCalDocument(); 109 * 110 * //ICalendar objects can be added at any time 111 * ICalendar ical = new ICalendar(); 112 * xcal.add(ical); 113 * 114 * //retrieving the raw XML DOM 115 * Document document = xcal.getDocument(); 116 * 117 * //call one of the "write()" methods to output the xCal document 118 * File file = new File("meeting.xml"); 119 * xcal.write(file); 120 * </pre> 121 * 122 * </p> 123 * @author Michael Angstadt 124 * @rfc 6321 125 */ 126 //@formatter:on 127 public class XCalDocument { 128 private static final ICalendarMarshaller icalMarshaller = ICalMarshallerRegistrar.getICalendarMarshaller(); 129 private static final XCalNamespaceContext nsContext = new XCalNamespaceContext("xcal"); 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 ICalMarshallerRegistrar registrar = new ICalMarshallerRegistrar(); 159 private final List<List<String>> parseWarnings = new ArrayList<List<String>>(); 160 private Document document; 161 private Element root; 162 163 /** 164 * Parses an xCal document from a string. 165 * @param xml the xCal document in the form of a string 166 * @throws SAXException if there's a problem parsing the XML 167 */ 168 public XCalDocument(String xml) throws SAXException { 169 this(XmlUtils.toDocument(xml)); 170 } 171 172 /** 173 * Parses an xCal document from an input stream. 174 * @param in the input stream to read the the xCal document from 175 * @throws IOException if there's a problem reading from the input stream 176 * @throws SAXException if there's a problem parsing the XML 177 */ 178 public XCalDocument(InputStream in) throws SAXException, IOException { 179 this(XmlUtils.toDocument(in)); 180 } 181 182 /** 183 * Parses an xCal document from a file. 184 * @param file the file containing the xCal document 185 * @throws IOException if there's a problem reading from the file 186 * @throws SAXException if there's a problem parsing the XML 187 */ 188 public XCalDocument(File file) throws SAXException, IOException { 189 InputStream in = new FileInputStream(file); 190 try { 191 init(XmlUtils.toDocument(in)); 192 } finally { 193 IOUtils.closeQuietly(in); 194 } 195 } 196 197 /** 198 * <p> 199 * Parses an xCal document from a reader. 200 * </p> 201 * <p> 202 * Note that use of this constructor is discouraged. It ignores the 203 * character encoding that is defined within the XML document itself, and 204 * should only be used if the encoding is undefined or if the encoding needs 205 * to be ignored for whatever reason. The {@link #XCalDocument(InputStream)} 206 * constructor should be used instead, since it takes the XML document's 207 * character encoding into account when parsing. 208 * </p> 209 * @param reader the reader to read the xCal document from 210 * @throws IOException if there's a problem reading from the reader 211 * @throws SAXException if there's a problem parsing the XML 212 */ 213 public XCalDocument(Reader reader) throws SAXException, IOException { 214 this(XmlUtils.toDocument(reader)); 215 } 216 217 /** 218 * Wraps an existing XML DOM object. 219 * @param document the XML DOM that contains the xCal document 220 */ 221 public XCalDocument(Document document) { 222 init(document); 223 } 224 225 /** 226 * Creates an empty xCal document. 227 */ 228 public XCalDocument() { 229 document = XmlUtils.createDocument(); 230 root = document.createElementNS(XCAL_NS, "icalendar"); 231 document.appendChild(root); 232 } 233 234 private void init(Document document) { 235 this.document = document; 236 237 XPath xpath = XPathFactory.newInstance().newXPath(); 238 xpath.setNamespaceContext(nsContext); 239 240 try { 241 //find the <icalendar> element 242 String prefix = nsContext.getPrefix(); 243 root = (Element) xpath.evaluate("//" + prefix + ":icalendar", document, XPathConstants.NODE); 244 } catch (XPathExpressionException e) { 245 //never thrown, xpath expression is hard coded 246 } 247 } 248 249 /** 250 * <p> 251 * Registers an experimental property marshaller. Can also be used to 252 * override the marshaller of a standard property (such as DTSTART). Calling 253 * this method is the same as calling: 254 * </p> 255 * <p> 256 * {@code getRegistrar().register(marshaller)}. 257 * </p> 258 * @param marshaller the marshaller to register 259 */ 260 public void registerMarshaller(ICalPropertyMarshaller<? extends ICalProperty> marshaller) { 261 registrar.register(marshaller); 262 } 263 264 /** 265 * <p> 266 * Registers an experimental component marshaller. Can also be used to 267 * override the marshaller of a standard component (such as VEVENT). Calling 268 * this method is the same as calling: 269 * </p> 270 * <p> 271 * {@code getRegistrar().register(marshaller)}. 272 * </p> 273 * @param marshaller the marshaller to register 274 */ 275 public void registerMarshaller(ICalComponentMarshaller<? extends ICalComponent> marshaller) { 276 registrar.register(marshaller); 277 } 278 279 /** 280 * Gets the object that manages the component/property marshaller objects. 281 * @return the marshaller registrar 282 */ 283 public ICalMarshallerRegistrar getRegistrar() { 284 return registrar; 285 } 286 287 /** 288 * Sets the object that manages the component/property marshaller objects. 289 * @param registrar the marshaller registrar 290 */ 291 public void setRegistrar(ICalMarshallerRegistrar registrar) { 292 this.registrar = registrar; 293 } 294 295 /** 296 * Registers the data type of an experimental parameter. Experimental 297 * parameters use the "unknown" xCal data type by default. 298 * @param parameterName the parameter name (e.g. "x-foo") 299 * @param dataType the data type or null to remove 300 */ 301 public void registerParameterDataType(String parameterName, ICalDataType dataType) { 302 parameterName = parameterName.toLowerCase(); 303 if (dataType == null) { 304 parameterDataTypes.remove(parameterName); 305 } else { 306 parameterDataTypes.put(parameterName, dataType); 307 } 308 } 309 310 /** 311 * Gets the raw XML DOM object. 312 * @return the XML DOM 313 */ 314 public Document getDocument() { 315 return document; 316 } 317 318 /** 319 * Gets the warnings from the last parse operation. 320 * @return the warnings (it is a "list of lists"--each parsed 321 * {@link ICalendar} object has its own warnings list) 322 * @see #parseAll 323 * @see #parseFirst 324 */ 325 public List<List<String>> getParseWarnings() { 326 return parseWarnings; 327 } 328 329 /** 330 * Parses all the {@link ICalendar} objects from the xCal document. 331 * @return the iCalendar objects 332 */ 333 public List<ICalendar> parseAll() { 334 parseWarnings.clear(); 335 336 if (root == null) { 337 return Collections.emptyList(); 338 } 339 340 List<ICalendar> icals = new ArrayList<ICalendar>(); 341 for (Element vcalendarElement : getVCalendarElements()) { 342 List<String> warnings = new ArrayList<String>(); 343 ICalendar ical = parseICal(vcalendarElement, warnings); 344 icals.add(ical); 345 this.parseWarnings.add(warnings); 346 } 347 348 return icals; 349 } 350 351 /** 352 * Parses the first {@link ICalendar} object from the xCal document. 353 * @return the iCalendar object or null if there are none 354 */ 355 public ICalendar parseFirst() { 356 parseWarnings.clear(); 357 358 if (root == null) { 359 return null; 360 } 361 362 List<String> warnings = new ArrayList<String>(); 363 parseWarnings.add(warnings); 364 365 List<Element> vcalendarElements = getVCalendarElements(); 366 if (vcalendarElements.isEmpty()) { 367 return null; 368 } 369 return parseICal(vcalendarElements.get(0), warnings); 370 } 371 372 /** 373 * Adds an iCalendar object to the xCal document. This marshals the 374 * {@link ICalendar} object to the XML DOM. This means that any changes that 375 * are made to the {@link ICalendar} object after calling this method will 376 * NOT be applied to the xCal document. 377 * @param ical the iCalendar object to add 378 * @throws IllegalArgumentException if the marshaller class for a component 379 * or property object cannot be found (only happens when an experimental 380 * property/component marshaller is not registered with the 381 * {@code registerMarshaller} method.) 382 */ 383 public void add(ICalendar ical) { 384 Element element = buildComponentElement(ical); 385 if (root == null) { 386 root = document.createElementNS(XCAL_NS, "icalendar"); 387 document.appendChild(root); 388 } 389 root.appendChild(element); 390 } 391 392 /** 393 * Writes the xCal document to a string without pretty-printing it. 394 * @return the XML string 395 */ 396 public String write() { 397 return write(-1); 398 } 399 400 /** 401 * Writes the xCal document to a string and pretty-prints it. 402 * @param indent the number of indent spaces to use for pretty-printing 403 * @return the XML string 404 */ 405 public String write(int indent) { 406 StringWriter sw = new StringWriter(); 407 try { 408 write(sw, indent); 409 } catch (TransformerException e) { 410 //writing to string 411 } 412 return sw.toString(); 413 } 414 415 /** 416 * Writes the xCal document to an output stream without pretty-printing it. 417 * @param out the output stream 418 * @throws TransformerException if there's a problem writing to the output 419 * stream 420 */ 421 public void write(OutputStream out) throws TransformerException { 422 write(out, -1); 423 } 424 425 /** 426 * Writes the xCal document to an output stream and pretty-prints it. 427 * @param out the output stream 428 * @param indent the number of indent spaces to use for pretty-printing 429 * @throws TransformerException if there's a problem writing to the output 430 * stream 431 */ 432 public void write(OutputStream out, int indent) throws TransformerException { 433 write(utf8Writer(out), indent); 434 } 435 436 /** 437 * Writes the xCal document to a file without pretty-printing it. 438 * @param file the file 439 * @throws IOException if there's a problem writing to the file 440 * @throws TransformerException if there's a problem writing the XML 441 */ 442 public void write(File file) throws TransformerException, IOException { 443 write(file, -1); 444 } 445 446 /** 447 * Writes the xCal document to a file and pretty-prints it. 448 * @param file the file stream 449 * @param indent the number of indent spaces to use for pretty-printing 450 * @throws IOException if there's a problem writing to the file 451 * @throws TransformerException if there's a problem writing the XML 452 */ 453 public void write(File file, int indent) throws TransformerException, IOException { 454 Writer writer = utf8Writer(file); 455 try { 456 write(writer, indent); 457 } finally { 458 IOUtils.closeQuietly(writer); 459 } 460 } 461 462 /** 463 * Writes the xCal document to a writer without pretty-printing it. 464 * @param writer the writer 465 * @throws TransformerException if there's a problem writing to the writer 466 */ 467 public void write(Writer writer) throws TransformerException { 468 write(writer, -1); 469 } 470 471 /** 472 * Writes the xCal document to a writer and pretty-prints it. 473 * @param writer the writer 474 * @param indent the number of indent spaces to use for pretty-printing 475 * @throws TransformerException if there's a problem writing to the writer 476 */ 477 public void write(Writer writer, int indent) throws TransformerException { 478 Map<String, String> properties = new HashMap<String, String>(); 479 if (indent >= 0) { 480 properties.put(OutputKeys.INDENT, "yes"); 481 properties.put("{http://xml.apache.org/xslt}indent-amount", indent + ""); 482 } 483 XmlUtils.toWriter(document, writer, properties); 484 } 485 486 @SuppressWarnings({ "rawtypes", "unchecked" }) 487 private Element buildComponentElement(ICalComponent component) { 488 ICalComponentMarshaller m = registrar.getComponentMarshaller(component); 489 if (m == null) { 490 throw new IllegalArgumentException("No marshaller found for component class \"" + component.getClass().getName() + "\"."); 491 } 492 493 Element componentElement = buildElement(m.getComponentName().toLowerCase()); 494 495 Element propertiesWrapperElement = buildElement("properties"); 496 for (Object obj : m.getProperties(component)) { 497 ICalProperty property = (ICalProperty) obj; 498 499 //create property element 500 Element propertyElement = buildPropertyElement(property); 501 if (propertyElement != null) { 502 propertiesWrapperElement.appendChild(propertyElement); 503 } 504 } 505 if (propertiesWrapperElement.hasChildNodes()) { 506 componentElement.appendChild(propertiesWrapperElement); 507 } 508 509 Element componentsWrapperElement = buildElement("components"); 510 for (Object obj : m.getComponents(component)) { 511 ICalComponent subComponent = (ICalComponent) obj; 512 Element subComponentElement = buildComponentElement(subComponent); 513 if (subComponentElement != null) { 514 componentsWrapperElement.appendChild(subComponentElement); 515 } 516 } 517 if (componentsWrapperElement.hasChildNodes()) { 518 componentElement.appendChild(componentsWrapperElement); 519 } 520 521 return componentElement; 522 } 523 524 @SuppressWarnings({ "rawtypes", "unchecked" }) 525 private Element buildPropertyElement(ICalProperty property) { 526 Element propertyElement; 527 ICalParameters parameters; 528 529 if (property instanceof Xml) { 530 Xml xml = (Xml) property; 531 532 Document value = xml.getValue(); 533 if (value == null) { 534 return null; 535 } 536 537 //import the XML element into the xCal DOM 538 propertyElement = XmlUtils.getRootElement(value); 539 propertyElement = (Element) document.importNode(propertyElement, true); 540 541 //get parameters 542 parameters = property.getParameters(); 543 } else { 544 ICalPropertyMarshaller pm = registrar.getPropertyMarshaller(property); 545 if (pm == null) { 546 throw new IllegalArgumentException("No marshaller found for property class \"" + property.getClass().getName() + "\"."); 547 } 548 549 propertyElement = buildElement(pm.getQName()); 550 551 //marshal value 552 try { 553 pm.writeXml(property, propertyElement); 554 } catch (SkipMeException e) { 555 return null; 556 } 557 558 //get parameters 559 parameters = pm.prepareParameters(property); 560 } 561 562 //build parameters 563 Element parametersWrapperElement = buildParametersElement(parameters); 564 if (parametersWrapperElement.hasChildNodes()) { 565 propertyElement.insertBefore(parametersWrapperElement, propertyElement.getFirstChild()); 566 } 567 568 return propertyElement; 569 } 570 571 private Element buildParametersElement(ICalParameters parameters) { 572 Element parametersWrapperElement = buildElement("parameters"); 573 574 for (Map.Entry<String, List<String>> parameter : parameters) { 575 String name = parameter.getKey().toLowerCase(); 576 ICalDataType dataType = parameterDataTypes.get(name); 577 String dataTypeStr = (dataType == null) ? "unknown" : dataType.getName().toLowerCase(); 578 579 Element parameterElement = buildAndAppendElement(name, parametersWrapperElement); 580 for (String parameterValue : parameter.getValue()) { 581 Element parameterValueElement = buildAndAppendElement(dataTypeStr, parameterElement); 582 parameterValueElement.setTextContent(parameterValue); 583 } 584 } 585 586 return parametersWrapperElement; 587 } 588 589 private ICalendar parseICal(Element icalElement, List<String> warnings) { 590 ICalComponent root = parseComponent(icalElement, warnings); 591 592 ICalendar ical; 593 if (root instanceof ICalendar) { 594 ical = (ICalendar) root; 595 } else { 596 //shouldn't happen, since only <vcalendar> elements are passed into this method 597 ical = icalMarshaller.emptyInstance(); 598 ical.addComponent(root); 599 } 600 return ical; 601 } 602 603 private ICalComponent parseComponent(Element componentElement, List<String> warnings) { 604 //create the component object 605 ICalComponentMarshaller<? extends ICalComponent> m = registrar.getComponentMarshaller(componentElement.getLocalName()); 606 ICalComponent component = m.emptyInstance(); 607 608 //parse properties 609 for (Element propertyWrapperElement : getChildElements(componentElement, "properties")) { //there should be only one <properties> element, but parse them all incase there are more 610 for (Element propertyElement : XmlUtils.toElementList(propertyWrapperElement.getChildNodes())) { 611 ICalProperty property = parseProperty(propertyElement, warnings); 612 if (property != null) { 613 component.addProperty(property); 614 } 615 } 616 } 617 618 //parse sub-components 619 for (Element componentWrapperElement : getChildElements(componentElement, "components")) { //there should be only one <components> element, but parse them all incase there are more 620 for (Element subComponentElement : XmlUtils.toElementList(componentWrapperElement.getChildNodes())) { 621 if (!XCAL_NS.equals(subComponentElement.getNamespaceURI())) { 622 continue; 623 } 624 625 ICalComponent subComponent = parseComponent(subComponentElement, warnings); 626 component.addComponent(subComponent); 627 } 628 } 629 630 return component; 631 } 632 633 private ICalProperty parseProperty(Element propertyElement, List<String> warnings) { 634 ICalParameters parameters = parseParameters(propertyElement); 635 String propertyName = propertyElement.getLocalName(); 636 QName qname = new QName(propertyElement.getNamespaceURI(), propertyName); 637 638 ICalPropertyMarshaller<? extends ICalProperty> m = registrar.getPropertyMarshaller(qname); 639 640 ICalProperty property = null; 641 try { 642 Result<? extends ICalProperty> result = m.parseXml(propertyElement, parameters); 643 644 for (String warning : result.getWarnings()) { 645 addWarning(warning, propertyName, warnings); 646 } 647 648 property = result.getProperty(); 649 } catch (SkipMeException e) { 650 if (e.getMessage() == null) { 651 addWarning("Property has requested that it be skipped.", propertyName, warnings); 652 } else { 653 addWarning("Property has requested that it be skipped: " + e.getMessage(), propertyName, warnings); 654 } 655 return null; 656 } catch (CannotParseException e) { 657 if (e.getMessage() == null) { 658 addWarning("Property could not be unmarshalled. Unmarshalling as an " + Xml.class.getSimpleName() + " property instead.", propertyName, warnings); 659 } else { 660 addWarning("Property could not be unmarshalled. Unmarshalling as an " + Xml.class.getSimpleName() + " property instead: " + e.getMessage(), propertyName, warnings); 661 } 662 } 663 664 //unmarshal as an XML property 665 if (property == null) { 666 m = registrar.getPropertyMarshaller(Xml.class); 667 668 Result<? extends ICalProperty> result = m.parseXml(propertyElement, parameters); 669 670 for (String warning : result.getWarnings()) { 671 addWarning(warning, propertyName, warnings); 672 } 673 674 property = result.getProperty(); 675 } 676 677 return property; 678 } 679 680 private ICalParameters parseParameters(Element propertyElement) { 681 ICalParameters parameters = new ICalParameters(); 682 683 for (Element parametersElement : getChildElements(propertyElement, "parameters")) { //there should be only one <parameters> element, but parse them all incase there are more 684 List<Element> paramElements = XmlUtils.toElementList(parametersElement.getChildNodes()); 685 for (Element paramElement : paramElements) { 686 String name = paramElement.getLocalName().toUpperCase(); 687 List<Element> valueElements = XmlUtils.toElementList(paramElement.getChildNodes()); 688 if (valueElements.isEmpty()) { //this should never be true if the xCal follows the specs 689 String value = paramElement.getTextContent(); 690 parameters.put(name, value); 691 } else { 692 for (Element valueElement : valueElements) { 693 String value = valueElement.getTextContent(); 694 parameters.put(name, value); 695 } 696 } 697 } 698 } 699 700 return parameters; 701 } 702 703 private Element buildElement(String localName) { 704 return buildElement(new QName(XCAL_NS, localName)); 705 } 706 707 private Element buildElement(QName qname) { 708 return document.createElementNS(qname.getNamespaceURI(), qname.getLocalPart()); 709 } 710 711 private Element buildAndAppendElement(String localName, Element parent) { 712 return buildAndAppendElement(new QName(XCAL_NS, localName), parent); 713 } 714 715 private Element buildAndAppendElement(QName qname, Element parent) { 716 Element child = document.createElementNS(qname.getNamespaceURI(), qname.getLocalPart()); 717 parent.appendChild(child); 718 return child; 719 } 720 721 private List<Element> getVCalendarElements() { 722 return getChildElements(root, "vcalendar"); 723 } 724 725 private List<Element> getChildElements(Element parent, String localName) { 726 List<Element> elements = new ArrayList<Element>(); 727 for (Element child : XmlUtils.toElementList(parent.getChildNodes())) { 728 if (localName.equals(child.getLocalName()) && XCAL_NS.equals(child.getNamespaceURI())) { 729 elements.add(child); 730 } 731 } 732 return elements; 733 } 734 735 private void addWarning(String message, String propertyName, List<String> warnings) { 736 warnings.add("<" + propertyName + "> property: " + message); 737 } 738 739 @Override 740 public String toString() { 741 return write(2); 742 } 743 }