001    package ezvcard.io.xml;
002    
003    import static ezvcard.util.IOUtils.utf8Writer;
004    
005    import java.io.File;
006    import java.io.FileInputStream;
007    import java.io.IOException;
008    import java.io.InputStream;
009    import java.io.OutputStream;
010    import java.io.Reader;
011    import java.io.StringWriter;
012    import java.io.Writer;
013    import java.util.ArrayList;
014    import java.util.Collections;
015    import java.util.HashMap;
016    import java.util.List;
017    import java.util.Map;
018    
019    import javax.xml.namespace.QName;
020    import javax.xml.transform.OutputKeys;
021    import javax.xml.transform.TransformerException;
022    import javax.xml.xpath.XPath;
023    import javax.xml.xpath.XPathConstants;
024    import javax.xml.xpath.XPathExpressionException;
025    import javax.xml.xpath.XPathFactory;
026    
027    import org.w3c.dom.Document;
028    import org.w3c.dom.Element;
029    import org.xml.sax.SAXException;
030    
031    import ezvcard.Ezvcard;
032    import ezvcard.Messages;
033    import ezvcard.VCard;
034    import ezvcard.VCardDataType;
035    import ezvcard.VCardVersion;
036    import ezvcard.io.CannotParseException;
037    import ezvcard.io.EmbeddedVCardException;
038    import ezvcard.io.SkipMeException;
039    import ezvcard.io.scribe.ScribeIndex;
040    import ezvcard.io.scribe.VCardPropertyScribe;
041    import ezvcard.io.scribe.VCardPropertyScribe.Result;
042    import ezvcard.parameter.VCardParameters;
043    import ezvcard.property.ProductId;
044    import ezvcard.property.VCardProperty;
045    import ezvcard.property.Xml;
046    import ezvcard.util.IOUtils;
047    import ezvcard.util.ListMultimap;
048    import ezvcard.util.XmlUtils;
049    
050    /*
051     Copyright (c) 2013, Michael Angstadt
052     All rights reserved.
053    
054     Redistribution and use in source and binary forms, with or without
055     modification, are permitted provided that the following conditions are met: 
056    
057     1. Redistributions of source code must retain the above copyright notice, this
058     list of conditions and the following disclaimer. 
059     2. Redistributions in binary form must reproduce the above copyright notice,
060     this list of conditions and the following disclaimer in the documentation
061     and/or other materials provided with the distribution. 
062    
063     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
064     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
065     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
066     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
067     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
068     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
069     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
070     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
071     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
072     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
073    
074     The views and conclusions contained in the software and documentation are those
075     of the authors and should not be interpreted as representing official policies, 
076     either expressed or implied, of the FreeBSD Project.
077     */
078    
079    //@formatter:off
080    /**
081    * <p>
082    * Represents an XML document that contains vCard objects ("xCard" standard).
083    * This class can be used to read and write xCard documents.
084    * </p>
085    * <p>
086    * <b>Examples:</b>
087    * 
088    * <pre class="brush:java">
089    * String xml =
090    * "&lt;vcards xmlns=\"urn:ietf:params:xml:ns:vcard-4.0\"&gt;" +
091    *   "&lt;vcard&gt;" +
092    *     "&lt;fn&gt;" +
093    *       "&lt;text&gt;John Doe&lt;/text&gt;" +
094    *     "&lt;/fn&gt;" +
095    *     "&lt;n&gt;" +
096    *       "&lt;surname&gt;Doe&lt;/surname&gt;" +
097    *        "&lt;given&gt;Johnathan&lt;/given&gt;" +
098    *        "&lt;additional&gt;Jonny&lt;/additional&gt;" +
099    *        "&lt;additional&gt;John&lt;/additional&gt;" +
100    *        "&lt;prefix&gt;Mr.&lt;/prefix&gt;" +
101    *        "&lt;suffix /&gt;" +
102    *      "&lt;/n&gt;" +
103    *    "&lt;/vcard&gt;" +
104    * "&lt;/vcards&gt;";
105    *     
106    * //parsing an existing xCard document
107    * XCardDocument xcard = new XCardDocument(xml);
108    * List&lt;VCard&gt; vcards = xcard.parseAll();
109    * 
110    * //creating an empty xCard document
111    * XCardDocument xcard = new XCardDocument();
112    * 
113    * //VCard objects can be added at any time
114    * VCard vcard = ...
115    * xcard.add(vcard);
116    * 
117    * //retrieving the raw XML DOM
118    * Document document = xcard.getDocument();
119    * 
120    * //call one of the "write()" methods to output the xCard document
121    * File file = new File("johndoe.xml");
122    * xcard.write(file);
123    * </pre>
124    * 
125    * </p>
126    * @author Michael Angstadt
127    * @see <a href="http://tools.ietf.org/html/rfc6351">RFC 6351</a>
128    */
129    //@formatter:on
130    public class XCardDocument {
131            private static final VCardVersion version4 = VCardVersion.V4_0; //xCard only supports 4.0
132            private static final XCardNamespaceContext nsContext = new XCardNamespaceContext(version4, "v");
133    
134            /**
135             * Defines the names of the XML elements that are used to hold each
136             * parameter's value.
137             */
138            private final Map<String, VCardDataType> parameterDataTypes = new HashMap<String, VCardDataType>();
139            {
140                    registerParameterDataType(VCardParameters.ALTID, VCardDataType.TEXT);
141                    registerParameterDataType(VCardParameters.CALSCALE, VCardDataType.TEXT);
142                    registerParameterDataType(VCardParameters.GEO, VCardDataType.URI);
143                    registerParameterDataType(VCardParameters.LABEL, VCardDataType.TEXT);
144                    registerParameterDataType(VCardParameters.LANGUAGE, VCardDataType.LANGUAGE_TAG);
145                    registerParameterDataType(VCardParameters.MEDIATYPE, VCardDataType.TEXT);
146                    registerParameterDataType(VCardParameters.PID, VCardDataType.TEXT);
147                    registerParameterDataType(VCardParameters.PREF, VCardDataType.INTEGER);
148                    registerParameterDataType(VCardParameters.SORT_AS, VCardDataType.TEXT);
149                    registerParameterDataType(VCardParameters.TYPE, VCardDataType.TEXT);
150                    registerParameterDataType(VCardParameters.TZ, VCardDataType.URI);
151            }
152    
153            private ScribeIndex index = new ScribeIndex();
154            private boolean addProdId = true;
155            private boolean versionStrict = true;
156            private final List<List<String>> parseWarnings = new ArrayList<List<String>>();
157            private Document document;
158            private Element root;
159    
160            /**
161             * Creates an xCard document.
162             */
163            public XCardDocument() {
164                    document = XmlUtils.createDocument();
165                    root = createElement("vcards");
166                    document.appendChild(root);
167            }
168    
169            /**
170             * Parses an xCard document from a string.
171             * @param xml the XML string to read the vCards from
172             * @throws SAXException if there's a problem parsing the XML
173             */
174            public XCardDocument(String xml) throws SAXException {
175                    this(XmlUtils.toDocument(xml));
176            }
177    
178            /**
179             * Parses an xCard document from an input stream.
180             * @param in the input stream to read the vCards from
181             * @throws IOException if there's a problem reading from the input stream
182             * @throws SAXException if there's a problem parsing the XML
183             */
184            public XCardDocument(InputStream in) throws SAXException, IOException {
185                    this(XmlUtils.toDocument(in));
186            }
187    
188            /**
189             * Parses an xCard document from a file.
190             * @param file the file to read the vCards from
191             * @throws IOException if there's a problem reading from the file
192             * @throws SAXException if there's a problem parsing the XML
193             */
194            public XCardDocument(File file) throws SAXException, IOException {
195                    InputStream in = new FileInputStream(file);
196                    try {
197                            init(XmlUtils.toDocument(in));
198                    } finally {
199                            IOUtils.closeQuietly(in);
200                    }
201            }
202    
203            /**
204             * <p>
205             * Parses an xCard document from a reader.
206             * </p>
207             * <p>
208             * Note that use of this constructor is discouraged. It ignores the
209             * character encoding that is defined within the XML document itself, and
210             * should only be used if the encoding is undefined or if the encoding needs
211             * to be ignored for whatever reason. The
212             * {@link #XCardDocument(InputStream)} constructor should be used instead,
213             * since it takes the XML document's character encoding into account when
214             * parsing.
215             * </p>
216             * @param reader the reader to read the vCards from
217             * @throws IOException if there's a problem reading from the reader
218             * @throws SAXException if there's a problem parsing the XML
219             */
220            public XCardDocument(Reader reader) throws SAXException, IOException {
221                    this(XmlUtils.toDocument(reader));
222            }
223    
224            /**
225             * Wraps an existing XML DOM object.
226             * @param document the XML DOM that contains the xCard document
227             */
228            public XCardDocument(Document document) {
229                    init(document);
230            }
231    
232            private void init(Document document) {
233                    XPath xpath = XPathFactory.newInstance().newXPath();
234                    xpath.setNamespaceContext(nsContext);
235    
236                    try {
237                            //find the <vcards> element
238                            String prefix = nsContext.getPrefix();
239                            root = (Element) xpath.evaluate("//" + prefix + ":vcards", document, XPathConstants.NODE);
240                    } catch (XPathExpressionException e) {
241                            //never thrown, xpath expression is hard coded
242                    }
243            }
244    
245            /**
246             * Gets whether or not a "PRODID" property will be added to each vCard,
247             * saying that the vCard was generated by this library.
248             * @return true if the property will be added, false if not (defaults to
249             * true)
250             */
251            public boolean isAddProdId() {
252                    return addProdId;
253            }
254    
255            /**
256             * Sets whether or not to add a "PRODID" property to each vCard, saying that
257             * the vCard was generated by this library.
258             * @param addProdId true to add this property, false not to (defaults to
259             * true)
260             */
261            public void setAddProdId(boolean addProdId) {
262                    this.addProdId = addProdId;
263            }
264    
265            /**
266             * Gets whether properties that do not support xCard (vCard version 4.0)
267             * will be excluded from the written vCard.
268             * @return true to exclude properties that do not support xCard, false to
269             * include them anyway (defaults to true)
270             */
271            public boolean isVersionStrict() {
272                    return versionStrict;
273            }
274    
275            /**
276             * Sets whether properties that do not support xCard (vCard version 4.0)
277             * will be excluded from the written vCard.
278             * @param versionStrict true to exclude properties that do not support
279             * xCard, false to include them anyway (defaults to true)
280             */
281            public void setVersionStrict(boolean versionStrict) {
282                    this.versionStrict = versionStrict;
283            }
284    
285            /**
286             * Gets the warnings from the last parse operation.
287             * @return the warnings (it is a "list of lists"--each parsed {@link VCard}
288             * object has its own warnings list)
289             * @see #parseAll
290             * @see #parseFirst
291             */
292            public List<List<String>> getParseWarnings() {
293                    return parseWarnings;
294            }
295    
296            /**
297             * Registers the data type of an experimental parameter. Experimental
298             * parameters use the "unknown" data type by default.
299             * @param parameterName the parameter name (e.g. "x-foo")
300             * @param dataType the data type or null to remove
301             */
302            public void registerParameterDataType(String parameterName, VCardDataType dataType) {
303                    parameterName = parameterName.toLowerCase();
304                    if (dataType == null) {
305                            parameterDataTypes.remove(parameterName);
306                    } else {
307                            parameterDataTypes.put(parameterName, dataType);
308                    }
309            }
310    
311            /**
312             * <p>
313             * Registers a property scribe. This is the same as calling:
314             * </p>
315             * <p>
316             * {@code getScribeIndex().register(scribe)}
317             * </p>
318             * @param scribe the scribe to register
319             */
320            public void registerScribe(VCardPropertyScribe<? extends VCardProperty> scribe) {
321                    index.register(scribe);
322            }
323    
324            /**
325             * Gets the scribe index.
326             * @return the scribe index
327             */
328            public ScribeIndex getScribeIndex() {
329                    return index;
330            }
331    
332            /**
333             * Sets the scribe index.
334             * @param index the scribe index
335             */
336            public void setScribeIndex(ScribeIndex index) {
337                    this.index = index;
338            }
339    
340            /**
341             * Gets the XML document that was generated.
342             * @return the XML document
343             */
344            public Document getDocument() {
345                    return document;
346            }
347    
348            /**
349             * Parses all the {@link VCard} objects from the xCard document.
350             * @return the vCard objects
351             */
352            public List<VCard> parseAll() {
353                    parseWarnings.clear();
354    
355                    if (root == null) {
356                            return Collections.emptyList();
357                    }
358    
359                    List<VCard> vcards = new ArrayList<VCard>();
360                    for (Element vcardElement : getVCardElements()) {
361                            List<String> warnings = new ArrayList<String>();
362                            parseWarnings.add(warnings);
363                            VCard vcard = parseVCardElement(vcardElement, warnings);
364                            vcards.add(vcard);
365                    }
366    
367                    return vcards;
368            }
369    
370            /**
371             * Parses the first the {@link VCard} object from the xCard document.
372             * @return the vCard object
373             */
374            public VCard parseFirst() {
375                    parseWarnings.clear();
376    
377                    if (root == null) {
378                            return null;
379                    }
380    
381                    List<Element> vcardElements = getVCardElements();
382                    if (vcardElements.isEmpty()) {
383                            return null;
384                    }
385    
386                    List<String> warnings = new ArrayList<String>();
387                    parseWarnings.add(warnings);
388                    return parseVCardElement(vcardElements.get(0), warnings);
389            }
390    
391            private VCard parseVCardElement(Element vcardElement, List<String> warnings) {
392                    VCard vcard = new VCard();
393                    vcard.setVersion(version4);
394    
395                    String ns = version4.getXmlNamespace();
396                    List<Element> children = XmlUtils.toElementList(vcardElement.getChildNodes());
397                    for (Element child : children) {
398                            if ("group".equals(child.getLocalName()) && ns.equals(child.getNamespaceURI())) {
399                                    String group = child.getAttribute("name");
400                                    if (group.length() == 0) {
401                                            group = null;
402                                    }
403                                    List<Element> propElements = XmlUtils.toElementList(child.getChildNodes());
404                                    for (Element propElement : propElements) {
405                                            parseAndAddElement(propElement, group, vcard, warnings);
406                                    }
407                            } else {
408                                    parseAndAddElement(child, null, vcard, warnings);
409                            }
410                    }
411    
412                    return vcard;
413            }
414    
415            /**
416             * Parses a property element from the XML document and adds the property to
417             * the vCard.
418             * @param element the element to parse
419             * @param group the group name or null if the property does not belong to a
420             * group
421             * @param vcard the vCard object
422             * @param warningsBuf the list to add the warnings to
423             */
424            private void parseAndAddElement(Element element, String group, VCard vcard, List<String> warnings) {
425                    VCardParameters parameters = parseSubTypes(element);
426    
427                    VCardProperty property;
428                    String propertyName = element.getLocalName();
429                    String ns = element.getNamespaceURI();
430                    VCardPropertyScribe<? extends VCardProperty> scribe = index.getPropertyScribe(new QName(ns, propertyName));
431                    try {
432                            Result<? extends VCardProperty> result = scribe.parseXml(element, parameters);
433    
434                            property = result.getProperty();
435                            property.setGroup(group);
436    
437                            for (String warning : result.getWarnings()) {
438                                    addWarning(propertyName, warning, warnings);
439                            }
440                    } catch (SkipMeException e) {
441                            addWarning(propertyName, warnings, 22, e.getMessage());
442                            return;
443                    } catch (CannotParseException e) {
444                            String xml = XmlUtils.toString(element);
445                            addWarning(propertyName, warnings, 33, xml, e.getMessage());
446    
447                            scribe = index.getPropertyScribe(Xml.class);
448                            Result<? extends VCardProperty> result = scribe.parseXml(element, parameters);
449                            property = result.getProperty();
450                            property.setGroup(group);
451                    } catch (EmbeddedVCardException e) {
452                            addWarning(propertyName, warnings, 34);
453                            return;
454                    }
455    
456                    vcard.addProperty(property);
457            }
458    
459            /**
460             * Parses the property parameters (aka "sub types").
461             * @param element the property's XML element
462             * @return the parsed parameters
463             */
464            private VCardParameters parseSubTypes(Element element) {
465                    VCardParameters subTypes = new VCardParameters();
466    
467                    List<Element> parametersElements = XmlUtils.toElementList(element.getElementsByTagNameNS(version4.getXmlNamespace(), "parameters"));
468                    for (Element parametersElement : parametersElements) { // foreach "<parameters>" element (there should only be 1 though)
469                            List<Element> paramElements = XmlUtils.toElementList(parametersElement.getChildNodes());
470                            for (Element paramElement : paramElements) {
471                                    String name = paramElement.getLocalName().toUpperCase();
472                                    List<Element> valueElements = XmlUtils.toElementList(paramElement.getChildNodes());
473                                    if (valueElements.isEmpty()) {
474                                            String value = paramElement.getTextContent();
475                                            subTypes.put(name, value);
476                                    } else {
477                                            for (Element valueElement : valueElements) {
478                                                    String value = valueElement.getTextContent();
479                                                    subTypes.put(name, value);
480                                            }
481                                    }
482                            }
483                    }
484    
485                    return subTypes;
486            }
487    
488            /**
489             * Writes the XML document to a string without pretty-printing it.
490             * @return the XML string
491             */
492            public String write() {
493                    return write(-1);
494            }
495    
496            /**
497             * Writes the XML document to a string and pretty-prints it.
498             * @param indent the number of indent spaces to use for pretty-printing
499             * @return the XML string
500             */
501            public String write(int indent) {
502                    StringWriter sw = new StringWriter();
503                    try {
504                            write(sw, indent);
505                    } catch (TransformerException e) {
506                            //writing to string
507                    }
508                    return sw.toString();
509            }
510    
511            /**
512             * Writes the XML document to an output stream without pretty-printing it.
513             * @param out the output stream
514             * @throws TransformerException if there's a problem writing to the output
515             * stream
516             */
517            public void write(OutputStream out) throws TransformerException {
518                    write(out, -1);
519            }
520    
521            /**
522             * Writes the XML document to an output stream and pretty-prints it.
523             * @param out the output stream
524             * @param indent the number of indent spaces to use for pretty-printing
525             * @throws TransformerException if there's a problem writing to the output
526             * stream
527             */
528            public void write(OutputStream out, int indent) throws TransformerException {
529                    write(utf8Writer(out), indent);
530            }
531    
532            /**
533             * Writes the XML document to a file without pretty-printing it.
534             * @param file the file
535             * @throws TransformerException if there's a problem writing to the file
536             * @throws IOException if there's a problem writing to the file
537             */
538            public void write(File file) throws TransformerException, IOException {
539                    write(file, -1);
540            }
541    
542            /**
543             * Writes the XML document to a file and pretty-prints it.
544             * @param file the file stream
545             * @param indent the number of indent spaces to use for pretty-printing
546             * @throws TransformerException if there's a problem writing to the file
547             * @throws IOException if there's a problem writing to the file
548             */
549            public void write(File file, int indent) throws TransformerException, IOException {
550                    Writer writer = utf8Writer(file);
551                    try {
552                            write(writer, indent);
553                    } finally {
554                            IOUtils.closeQuietly(writer);
555                    }
556            }
557    
558            /**
559             * Writes the XML document to a writer without pretty-printing it.
560             * @param writer the writer
561             * @throws TransformerException if there's a problem writing to the writer
562             */
563            public void write(Writer writer) throws TransformerException {
564                    write(writer, -1);
565            }
566    
567            /**
568             * Writes the XML document to a writer and pretty-prints it.
569             * @param writer the writer
570             * @param indent the number of indent spaces to use for pretty-printing
571             * @throws TransformerException if there's a problem writing to the writer
572             */
573            public void write(Writer writer, int indent) throws TransformerException {
574                    Map<String, String> properties = new HashMap<String, String>();
575                    if (indent >= 0) {
576                            properties.put(OutputKeys.INDENT, "yes");
577                            properties.put("{http://xml.apache.org/xslt}indent-amount", indent + "");
578                    }
579                    XmlUtils.toWriter(document, writer, properties);
580            }
581    
582            /**
583             * Adds a vCard to the XML document.
584             * @param vcard the vCard to add
585             * @throws IllegalArgumentException if a scribe hasn't been registered for a
586             * custom property class (see: {@link #registerScribe})
587             */
588            public void add(VCard vcard) {
589                    ListMultimap<String, VCardProperty> typesToAdd = new ListMultimap<String, VCardProperty>(); //group the types by group name (null = no group name)
590    
591                    for (VCardProperty type : vcard) {
592                            if (addProdId && type instanceof ProductId) {
593                                    //do not add the PRODID in the vCard if "addProdId" is true
594                                    continue;
595                            }
596    
597                            if (versionStrict && !type.getSupportedVersions().contains(version4)) {
598                                    //do not add the property to the vCard if it is not supported by the target version
599                                    continue;
600                            }
601    
602                            //check for scribes before writing anything to the stream
603                            if (index.getPropertyScribe(type) == null) {
604                                    throw new IllegalArgumentException("No scribe found for property class \"" + type.getClass().getName() + "\".");
605                            }
606    
607                            typesToAdd.put(type.getGroup(), type);
608                    }
609    
610                    //add an extended type saying it was generated by this library
611                    if (addProdId) {
612                            ProductId prodId = new ProductId("ez-vcard " + Ezvcard.VERSION);
613                            typesToAdd.put(prodId.getGroup(), prodId);
614                    }
615    
616                    //marshal each type object
617                    Element vcardElement = createElement("vcard");
618                    for (String groupName : typesToAdd.keySet()) {
619                            Element parent;
620                            if (groupName != null) {
621                                    Element groupElement = createElement("group");
622                                    groupElement.setAttribute("name", groupName);
623                                    vcardElement.appendChild(groupElement);
624                                    parent = groupElement;
625                            } else {
626                                    parent = vcardElement;
627                            }
628    
629                            for (VCardProperty type : typesToAdd.get(groupName)) {
630                                    try {
631                                            Element typeElement = marshalType(type, vcard);
632                                            parent.appendChild(typeElement);
633                                    } catch (SkipMeException e) {
634                                            //skip property
635                                    } catch (EmbeddedVCardException e) {
636                                            //skip property
637                                    }
638                            }
639                    }
640                    root.appendChild(vcardElement);
641            }
642    
643            /**
644             * Marshals a type object to an XML element.
645             * @param type the type object to marshal
646             * @param vcard the vcard the type belongs to
647             * @return the XML element
648             */
649            @SuppressWarnings({ "rawtypes", "unchecked" })
650            private Element marshalType(VCardProperty type, VCard vcard) {
651                    VCardPropertyScribe scribe = index.getPropertyScribe(type);
652                    if (scribe == null) {
653                            throw new IllegalArgumentException("No marshaller found for property class \"" + type.getClass().getName() + "\".");
654                    }
655    
656                    VCardParameters parameters = scribe.prepareParameters(type, version4, vcard);
657    
658                    QName qname = scribe.getQName();
659                    Element typeElement = createElement(qname.getLocalPart(), qname.getNamespaceURI());
660    
661                    //marshal the sub types
662                    if (!parameters.isEmpty()) {
663                            Element parametersElement = marshalSubTypes(parameters);
664                            typeElement.appendChild(parametersElement);
665                    }
666    
667                    //marshal the value
668                    scribe.writeXml(type, typeElement);
669    
670                    return typeElement;
671            }
672    
673            private Element marshalSubTypes(VCardParameters parameters) {
674                    Element parametersElement = createElement("parameters");
675    
676                    for (Map.Entry<String, List<String>> param : parameters) {
677                            String parameterName = param.getKey().toLowerCase();
678                            Element parameterElement = createElement(parameterName);
679    
680                            for (String paramValue : param.getValue()) {
681                                    VCardDataType dataType = parameterDataTypes.get(parameterName);
682                                    String dataTypeElementName = (dataType == null) ? "unknown" : dataType.getName().toLowerCase();
683                                    Element dataTypeElement = createElement(dataTypeElementName);
684                                    dataTypeElement.setTextContent(paramValue);
685                                    parameterElement.appendChild(dataTypeElement);
686                            }
687    
688                            parametersElement.appendChild(parameterElement);
689                    }
690    
691                    return parametersElement;
692            }
693    
694            private List<Element> getVCardElements() {
695                    return getChildElements(root, "vcard");
696            }
697    
698            private List<Element> getChildElements(Element parent, String localName) {
699                    List<Element> elements = new ArrayList<Element>();
700                    for (Element child : XmlUtils.toElementList(parent.getChildNodes())) {
701                            if (localName.equals(child.getLocalName()) && version4.getXmlNamespace().equals(child.getNamespaceURI())) {
702                                    elements.add(child);
703                            }
704                    }
705                    return elements;
706            }
707    
708            /**
709             * Creates a new XML element.
710             * @param name the name of the XML element
711             * @return the new XML element
712             */
713            private Element createElement(String name) {
714                    return createElement(name, version4.getXmlNamespace());
715            }
716    
717            /**
718             * Creates a new XML element.
719             * @param name the name of the XML element
720             * @param ns the namespace of the XML element
721             * @return the new XML element
722             */
723            private Element createElement(String name, String ns) {
724                    return document.createElementNS(ns, name);
725            }
726    
727            private void addWarning(String propertyName, List<String> warnings, int code, Object... args) {
728                    String message = Messages.INSTANCE.getParseMessage(code, args);
729                    addWarning(propertyName, message, warnings);
730            }
731    
732            private void addWarning(String propertyName, String message, List<String> warnings) {
733                    String warning = Messages.INSTANCE.getParseMessage(35, propertyName, message);
734                    warnings.add(warning);
735            }
736    }