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