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