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