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