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