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