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     * "&lt;?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
085     * "&lt;icalendar xmlns=\"urn:ietf:params:xml:ns:icalendar-2.0\"&gt;" +
086     *   "&lt;vcalendar&gt;" +
087     *     "&lt;properties&gt;" +
088     *       "&lt;prodid&gt;&lt;text&gt;-//Example Inc.//Example Client//EN&lt;/text&gt;&lt;/prodid&gt;" +
089     *       "&lt;version&gt;&lt;text&gt;2.0&lt;/text&gt;&lt;/version&gt;" +
090     *     "&lt;/properties&gt;" +
091     *     "&lt;components&gt;" +
092     *       "&lt;vevent&gt;" +
093     *         "&lt;properties&gt;" +
094     *           "&lt;dtstart&gt;&lt;date-time&gt;2013-06-27T13:00:00Z&lt;/date-time&gt;&lt;/dtstart&gt;" +
095     *           "&lt;dtend&gt;&lt;date-time&gt;2013-06-27T15:00:00Z&lt;/date-time&gt;&lt;/dtend&gt;" +
096     *           "&lt;summary&gt;&lt;text&gt;Team Meeting&lt;/text&gt;&lt;/summary&gt;" +
097     *         "&lt;/properties&gt;" +
098     *       "&lt;/vevent&gt;" +
099     *     "&lt;/components&gt;" +
100     *   "&lt;/vcalendar&gt;" +
101     * "&lt;/icalendar&gt;";
102     *     
103     * //parsing an existing xCal document
104     * XCalDocument xcal = new XCalDocument(xml);
105     * List&lt;ICalendar&gt; 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    }