001package ezvcard.io.xml;
002
003import static ezvcard.io.xml.XCardQNames.GROUP;
004import static ezvcard.io.xml.XCardQNames.PARAMETERS;
005import static ezvcard.io.xml.XCardQNames.VCARD;
006import static ezvcard.io.xml.XCardQNames.VCARDS;
007
008import java.io.IOException;
009import java.io.InputStream;
010import java.io.OutputStream;
011import java.io.OutputStreamWriter;
012import java.io.Reader;
013import java.io.StringWriter;
014import java.io.UncheckedIOException;
015import java.io.Writer;
016import java.nio.charset.StandardCharsets;
017import java.nio.file.Files;
018import java.nio.file.Path;
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.Iterator;
022import java.util.List;
023import java.util.Map;
024
025import javax.xml.namespace.QName;
026import javax.xml.transform.Transformer;
027import javax.xml.transform.TransformerConfigurationException;
028import javax.xml.transform.TransformerException;
029import javax.xml.transform.TransformerFactory;
030import javax.xml.transform.TransformerFactoryConfigurationError;
031import javax.xml.transform.dom.DOMSource;
032import javax.xml.transform.stream.StreamResult;
033import javax.xml.xpath.XPath;
034import javax.xml.xpath.XPathConstants;
035import javax.xml.xpath.XPathExpressionException;
036import javax.xml.xpath.XPathFactory;
037
038import org.w3c.dom.Document;
039import org.w3c.dom.Element;
040import org.w3c.dom.Node;
041import org.xml.sax.SAXException;
042
043import ezvcard.VCard;
044import ezvcard.VCardDataType;
045import ezvcard.VCardVersion;
046import ezvcard.io.CannotParseException;
047import ezvcard.io.EmbeddedVCardException;
048import ezvcard.io.ParseWarning;
049import ezvcard.io.SkipMeException;
050import ezvcard.io.StreamReader;
051import ezvcard.io.StreamWriter;
052import ezvcard.io.scribe.VCardPropertyScribe;
053import ezvcard.parameter.VCardParameters;
054import ezvcard.property.VCardProperty;
055import ezvcard.property.Xml;
056import ezvcard.util.ListMultimap;
057import ezvcard.util.XmlUtils;
058
059/*
060 Copyright (c) 2012-2023, Michael Angstadt
061 All rights reserved.
062
063 Redistribution and use in source and binary forms, with or without
064 modification, are permitted provided that the following conditions are met: 
065
066 1. Redistributions of source code must retain the above copyright notice, this
067 list of conditions and the following disclaimer. 
068 2. Redistributions in binary form must reproduce the above copyright notice,
069 this list of conditions and the following disclaimer in the documentation
070 and/or other materials provided with the distribution. 
071
072 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
073 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
074 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
075 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
076 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
077 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
078 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
079 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
080 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
081 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
082
083 The views and conclusions contained in the software and documentation are those
084 of the authors and should not be interpreted as representing official policies, 
085 either expressed or implied, of the FreeBSD Project.
086 */
087
088//@formatter:off
089/**
090* <p>
091* Represents an XML document that contains vCard objects ("xCard" standard).
092* This class can be used to read and write xCard documents.
093* </p>
094* <p>
095* <b>Examples:</b>
096* </p>
097* 
098* <pre class="brush:java">
099* String xml =
100* "&lt;vcards xmlns=\"urn:ietf:params:xml:ns:vcard-4.0\"&gt;" +
101*   "&lt;vcard&gt;" +
102*     "&lt;fn&gt;" +
103*       "&lt;text&gt;John Doe&lt;/text&gt;" +
104*     "&lt;/fn&gt;" +
105*     "&lt;n&gt;" +
106*       "&lt;surname&gt;Doe&lt;/surname&gt;" +
107*        "&lt;given&gt;Johnathan&lt;/given&gt;" +
108*        "&lt;additional&gt;Jonny&lt;/additional&gt;" +
109*        "&lt;additional&gt;John&lt;/additional&gt;" +
110*        "&lt;prefix&gt;Mr.&lt;/prefix&gt;" +
111*        "&lt;suffix /&gt;" +
112*      "&lt;/n&gt;" +
113*    "&lt;/vcard&gt;" +
114* "&lt;/vcards&gt;";
115*     
116* //parsing an existing xCard document
117* XCardDocument xcard = new XCardDocument(xml);
118* List&lt;VCard&gt; vcards = xcard.getVCards();
119* 
120* //creating an empty xCard document
121* XCardDocument xcard = new XCardDocument();
122* 
123* //VCard objects can be added at any time
124* VCard vcard = ...
125* xcard.addVCard(vcard);
126* 
127* //retrieving the raw XML DOM
128* Document document = xcard.getDocument();
129* 
130* //call one of the "write()" methods to output the xCard document
131* File file = new File("johndoe.xml");
132* xcard.write(file);
133* </pre>
134* @author Michael Angstadt
135* @see <a href="http://tools.ietf.org/html/rfc6351">RFC 6351</a>
136*/
137//@formatter:on
138public class XCardDocument {
139        private final VCardVersion version4 = VCardVersion.V4_0; //xCard only supports 4.0
140        private final Document document;
141        private Element vcardsRootElement;
142
143        /**
144         * Creates an empty xCard document.
145         */
146        public XCardDocument() {
147                document = XmlUtils.createDocument();
148                vcardsRootElement = document.createElementNS(VCARDS.getNamespaceURI(), VCARDS.getLocalPart());
149                document.appendChild(vcardsRootElement);
150        }
151
152        /**
153         * Parses an xCard document from a string.
154         * @param xml the XML string to read the vCards from
155         * @throws SAXException if there's a problem parsing the XML
156         */
157        public XCardDocument(String xml) throws SAXException {
158                this(XmlUtils.toDocument(xml));
159        }
160
161        /**
162         * Parses an xCard document from an input stream.
163         * @param in the input stream to read the vCards from
164         * @throws IOException if there's a problem reading from the input stream
165         * @throws SAXException if there's a problem parsing the XML
166         */
167        public XCardDocument(InputStream in) throws SAXException, IOException {
168                this(XmlUtils.toDocument(in));
169        }
170
171        /**
172         * Parses an xCard document from a file.
173         * @param file the file to read the vCards from
174         * @throws IOException if there's a problem reading from the file
175         * @throws SAXException if there's a problem parsing the XML
176         */
177        public XCardDocument(Path file) throws SAXException, IOException {
178                this(XmlUtils.toDocument(file));
179        }
180
181        /**
182         * <p>
183         * Parses an xCard document from a reader.
184         * </p>
185         * <p>
186         * Note that use of this constructor is discouraged. It ignores the
187         * character encoding that is defined within the XML document itself, and
188         * should only be used if the encoding is undefined or if the encoding needs
189         * to be ignored for whatever reason. The
190         * {@link #XCardDocument(InputStream)} constructor is preferred, since it
191         * takes the XML document's character encoding into account when parsing.
192         * </p>
193         * @param reader the reader to read the vCards from
194         * @throws IOException if there's a problem reading from the reader
195         * @throws SAXException if there's a problem parsing the XML
196         */
197        public XCardDocument(Reader reader) throws SAXException, IOException {
198                this(XmlUtils.toDocument(reader));
199        }
200
201        /**
202         * Wraps an existing XML DOM object.
203         * @param document the XML DOM that contains the xCard document
204         */
205        public XCardDocument(Document document) {
206                this.document = document;
207
208                XCardNamespaceContext nsContext = new XCardNamespaceContext(version4, "v");
209                XPath xpath = XPathFactory.newInstance().newXPath();
210                xpath.setNamespaceContext(nsContext);
211
212                try {
213                        //find the <vcards> element
214                        vcardsRootElement = (Element) xpath.evaluate("//" + nsContext.getPrefix() + ":" + VCARDS.getLocalPart(), document, XPathConstants.NODE);
215                } catch (XPathExpressionException e) {
216                        //should never thrown because the xpath expression is hard coded
217                        throw new RuntimeException(e);
218                }
219        }
220
221        /**
222         * Creates a {@link StreamReader} object that reads vCards from this XML
223         * document.
224         * @return the reader
225         */
226        public StreamReader reader() {
227                return new XCardDocumentStreamReader();
228        }
229
230        /**
231         * Creates a {@link StreamWriter} object that adds vCards to this XML
232         * document.
233         * @return the writer
234         */
235        public XCardDocumentStreamWriter writer() {
236                return new XCardDocumentStreamWriter();
237        }
238
239        /**
240         * Gets the wrapped XML document.
241         * @return the XML document
242         */
243        public Document getDocument() {
244                return document;
245        }
246
247        /**
248         * Parses all of the vCards from this XML document. Modifications made to
249         * these {@link VCard} objects will NOT be applied to the XML document.
250         * @return the parsed vCards
251         */
252        public List<VCard> getVCards() {
253                try {
254                        return reader().readAll();
255                } catch (IOException e) {
256                        //not thrown because we're reading from a DOM
257                        throw new UncheckedIOException(e);
258                }
259        }
260
261        /**
262         * Adds a vCard to the XML document.
263         * @param vcard the vCard to add
264         * @throws IllegalArgumentException if no scribe has been registered for the
265         * property (only applies to custom property classes)
266         */
267        public void addVCard(VCard vcard) {
268                writer().write(vcard);
269        }
270
271        /**
272         * Writes the XML document to a string.
273         * @return the XML string
274         */
275        public String write() {
276                return write((Integer) null);
277        }
278
279        /**
280         * Writes the XML document to a string.
281         * @param indent the number of indent spaces to use for pretty-printing or
282         * null to disable pretty-printing (disabled by default)
283         * @return the XML string
284         */
285        public String write(Integer indent) {
286                return write(indent, null);
287        }
288
289        /**
290         * Writes the XML document to a string.
291         * @param indent the number of indent spaces to use for pretty-printing or
292         * null to disable pretty-printing (disabled by default)
293         * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many
294         * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library
295         * like <a href=
296         * "http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22xalan%22%20AND%20a%3A%22xalan%22"
297         * >xalan</a> to your project)
298         * @return the XML string
299         */
300        public String write(Integer indent, String xmlVersion) {
301                return write(new XCardOutputProperties(indent, xmlVersion));
302        }
303
304        /**
305         * Writes the XML document to a string.
306         * @param outputProperties properties to assign to the JAXP transformer (see
307         * {@link Transformer#setOutputProperty})
308         * @return the XML string
309         */
310        public String write(Map<String, String> outputProperties) {
311                StringWriter sw = new StringWriter();
312                try {
313                        write(sw, outputProperties);
314                } catch (TransformerException e) {
315                        //should not be thrown because we're writing to a string
316                        throw new RuntimeException(e);
317                }
318                return sw.toString();
319        }
320
321        /**
322         * Writes the XML document to an output stream.
323         * @param out the output stream (UTF-8 encoding will be used)
324         * @throws TransformerException if there's a problem writing to the output
325         * stream
326         */
327        public void write(OutputStream out) throws TransformerException {
328                write(out, (Integer) null);
329        }
330
331        /**
332         * Writes the XML document to an output stream.
333         * @param out the output stream (UTF-8 encoding will be used)
334         * @param indent the number of indent spaces to use for pretty-printing or
335         * null to disable pretty-printing (disabled by default)
336         * @throws TransformerException if there's a problem writing to the output
337         * stream
338         */
339        public void write(OutputStream out, Integer indent) throws TransformerException {
340                write(out, indent, null);
341        }
342
343        /**
344         * Writes the XML document to an output stream.
345         * @param out the output stream (UTF-8 encoding will be used)
346         * @param indent the number of indent spaces to use for pretty-printing or
347         * null to disable pretty-printing (disabled by default)
348         * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many
349         * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library
350         * like <a href=
351         * "http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22xalan%22%20AND%20a%3A%22xalan%22"
352         * >xalan</a> to your project)
353         * @throws TransformerException if there's a problem writing to the output
354         * stream
355         */
356        public void write(OutputStream out, Integer indent, String xmlVersion) throws TransformerException {
357                write(out, new XCardOutputProperties(indent, xmlVersion));
358        }
359
360        /**
361         * Writes the XML document to an output stream.
362         * @param out the output stream (UTF-8 encoding will be used)
363         * @param outputProperties properties to assign to the JAXP transformer (see
364         * {@link Transformer#setOutputProperty})
365         * @throws TransformerException if there's a problem writing to the output
366         * stream
367         */
368        public void write(OutputStream out, Map<String, String> outputProperties) throws TransformerException {
369                write(new OutputStreamWriter(out, StandardCharsets.UTF_8), outputProperties);
370        }
371
372        /**
373         * Writes the XML document to a file.
374         * @param file the file to write to (UTF-8 encoding will be used)
375         * @throws TransformerException if there's a problem writing to the file
376         * @throws IOException if there's a problem writing to the file
377         */
378        public void write(Path file) throws TransformerException, IOException {
379                write(file, (Integer) null);
380        }
381
382        /**
383         * Writes the XML document to a file.
384         * @param file the file to write to (UTF-8 encoding will be used)
385         * @param indent the number of indent spaces to use for pretty-printing or
386         * null to disable pretty-printing (disabled by default)
387         * @throws TransformerException if there's a problem writing to the file
388         * @throws IOException if there's a problem writing to the file
389         */
390        public void write(Path file, Integer indent) throws TransformerException, IOException {
391                write(file, indent, null);
392        }
393
394        /**
395         * Writes the XML document to a file.
396         * @param file the file to write to (UTF-8 encoding will be used)
397         * @param indent the number of indent spaces to use for pretty-printing or
398         * null to disable pretty-printing (disabled by default)
399         * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many
400         * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library
401         * like <a href=
402         * "http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22xalan%22%20AND%20a%3A%22xalan%22"
403         * >xalan</a> to your project)
404         * @throws TransformerException if there's a problem writing to the file
405         * @throws IOException if there's a problem writing to the file
406         */
407        public void write(Path file, Integer indent, String xmlVersion) throws TransformerException, IOException {
408                write(file, new XCardOutputProperties(indent, xmlVersion));
409        }
410
411        /**
412         * Writes the XML document to a file.
413         * @param file the file to write to (UTF-8 encoding will be used)
414         * @param outputProperties properties to assign to the JAXP transformer (see
415         * {@link Transformer#setOutputProperty})
416         * @throws TransformerException if there's a problem writing to the file
417         * @throws IOException if there's a problem writing to the file
418         */
419        public void write(Path file, Map<String, String> outputProperties) throws TransformerException, IOException {
420                try (Writer writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
421                        write(writer, outputProperties);
422                }
423        }
424
425        /**
426         * Writes the XML document to a writer.
427         * @param writer the writer
428         * @throws TransformerException if there's a problem writing to the writer
429         */
430        public void write(Writer writer) throws TransformerException {
431                write(writer, (Integer) null);
432        }
433
434        /**
435         * Writes the XML document to a writer.
436         * @param writer the writer
437         * @param indent the number of indent spaces to use for pretty-printing or
438         * null to disable pretty-printing (disabled by default)
439         * @throws TransformerException if there's a problem writing to the writer
440         */
441        public void write(Writer writer, Integer indent) throws TransformerException {
442                write(writer, indent, null);
443        }
444
445        /**
446         * Writes the XML document to a writer.
447         * @param writer the writer
448         * @param indent the number of indent spaces to use for pretty-printing or
449         * null to disable pretty-printing (disabled by default)
450         * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many
451         * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library
452         * like <a href=
453         * "http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22xalan%22%20AND%20a%3A%22xalan%22"
454         * >xalan</a> to your project)
455         * @throws TransformerException if there's a problem writing to the writer
456         */
457        public void write(Writer writer, Integer indent, String xmlVersion) throws TransformerException {
458                write(writer, new XCardOutputProperties(indent, xmlVersion));
459        }
460
461        /**
462         * Writes the XML document to a writer.
463         * @param writer the writer
464         * @param outputProperties properties to assign to the JAXP transformer (see
465         * {@link Transformer#setOutputProperty})
466         * @throws TransformerException if there's a problem writing to the writer
467         */
468        public void write(Writer writer, Map<String, String> outputProperties) throws TransformerException {
469                Transformer transformer;
470                try {
471                        transformer = TransformerFactory.newInstance().newTransformer();
472                } catch (TransformerConfigurationException e) {
473                        //should never be thrown because we're not doing anything fancy with the configuration
474                        throw new RuntimeException(e);
475                } catch (TransformerFactoryConfigurationError e) {
476                        //should never be thrown because we're not doing anything fancy with the configuration
477                        throw new RuntimeException(e);
478                }
479
480                /*
481                 * Using Transformer#setOutputProperties(Properties) doesn't work for
482                 * some reason for setting the number of indentation spaces.
483                 */
484                for (Map.Entry<String, String> entry : outputProperties.entrySet()) {
485                        String key = entry.getKey();
486                        String value = entry.getValue();
487                        transformer.setOutputProperty(key, value);
488                }
489
490                DOMSource source = new DOMSource(document);
491                StreamResult result = new StreamResult(writer);
492                transformer.transform(source, result);
493        }
494
495        private class XCardDocumentStreamReader extends StreamReader {
496                private final Iterator<Element> vcardElements;
497                {
498                        List<Element> list = (vcardsRootElement == null) ? Collections.<Element> emptyList() : getChildElements(vcardsRootElement, VCARD);
499                        vcardElements = list.iterator();
500                }
501
502                private VCard vcard;
503
504                @Override
505                public VCard readNext() {
506                        try {
507                                return super.readNext();
508                        } catch (IOException e) {
509                                //will not be thrown
510                                throw new UncheckedIOException(e);
511                        }
512                }
513
514                @Override
515                protected VCard _readNext() throws IOException {
516                        if (!vcardElements.hasNext()) {
517                                return null;
518                        }
519
520                        vcard = new VCard();
521                        vcard.setVersion(version4);
522                        context.setVersion(version4);
523                        parseVCardElement(vcardElements.next());
524                        return vcard;
525                }
526
527                public void close() {
528                        //empty
529                }
530
531                private void parseVCardElement(Element vcardElement) {
532                        List<Element> children = XmlUtils.toElementList(vcardElement.getChildNodes());
533                        for (Element child : children) {
534                                if (XmlUtils.hasQName(child, GROUP)) {
535                                        String group = child.getAttribute("name");
536                                        if (group.isEmpty()) {
537                                                group = null;
538                                        }
539                                        List<Element> grandChildren = XmlUtils.toElementList(child.getChildNodes());
540                                        for (Element grandChild : grandChildren) {
541                                                parseAndAddElement(grandChild, group);
542                                        }
543                                        continue;
544                                }
545
546                                parseAndAddElement(child, null);
547                        }
548                }
549
550                /**
551                 * Parses a property element from the XML document and adds the property
552                 * to the vCard.
553                 * @param element the element to parse
554                 * @param group the group name or null if the property does not belong
555                 * to a group
556                 */
557                private void parseAndAddElement(Element element, String group) {
558                        VCardParameters parameters = parseParameters(element);
559
560                        VCardProperty property;
561                        String propertyName = element.getLocalName();
562                        String ns = element.getNamespaceURI();
563                        QName qname = new QName(ns, propertyName);
564                        VCardPropertyScribe<? extends VCardProperty> scribe = index.getPropertyScribe(qname);
565
566                        context.getWarnings().clear();
567                        context.setPropertyName(propertyName);
568                        try {
569                                property = scribe.parseXml(element, parameters, context);
570                                property.setGroup(group);
571                                warnings.addAll(context.getWarnings());
572                        } catch (SkipMeException e) {
573                                //@formatter:off
574                                warnings.add(new ParseWarning.Builder(context)
575                                        .message(22, e.getMessage())
576                                        .build()
577                                );
578                                //@formatter:on
579                                return;
580                        } catch (CannotParseException e) {
581                                //@formatter:off
582                                warnings.add(new ParseWarning.Builder(context)
583                                        .message(e)
584                                        .build()
585                                );
586                                //@formatter:on
587
588                                scribe = index.getPropertyScribe(Xml.class);
589                                property = scribe.parseXml(element, parameters, context);
590                                property.setGroup(group);
591                        } catch (EmbeddedVCardException e) {
592                                //@formatter:off
593                                warnings.add(new ParseWarning.Builder(context)
594                                        .message(34)
595                                        .build()
596                                );
597                                //@formatter:on
598                                return;
599                        }
600
601                        vcard.addProperty(property);
602                }
603
604                /**
605                 * Parses the property parameters.
606                 * @param element the property's XML element
607                 * @return the parsed parameters
608                 */
609                private VCardParameters parseParameters(Element element) {
610                        VCardParameters parameters = new VCardParameters();
611
612                        List<Element> roots = XmlUtils.toElementList(element.getElementsByTagNameNS(PARAMETERS.getNamespaceURI(), PARAMETERS.getLocalPart()));
613                        for (Element root : roots) { // foreach "<parameters>" element (there should only be 1 though)
614                                List<Element> parameterElements = XmlUtils.toElementList(root.getChildNodes());
615                                for (Element parameterElement : parameterElements) {
616                                        String name = parameterElement.getLocalName().toUpperCase();
617                                        List<Element> valueElements = XmlUtils.toElementList(parameterElement.getChildNodes());
618                                        for (Element valueElement : valueElements) {
619                                                String value = valueElement.getTextContent();
620                                                parameters.put(name, value);
621                                        }
622                                }
623                        }
624
625                        return parameters;
626                }
627
628                private List<Element> getChildElements(Element parent, QName qname) {
629                        List<Element> elements = new ArrayList<>();
630                        for (Element child : XmlUtils.toElementList(parent.getChildNodes())) {
631                                if (XmlUtils.hasQName(child, qname)) {
632                                        elements.add(child);
633                                }
634                        }
635                        return elements;
636                }
637        }
638
639        public class XCardDocumentStreamWriter extends XCardWriterBase {
640                @Override
641                public void write(VCard vcard) {
642                        try {
643                                super.write(vcard);
644                        } catch (IOException ignore) {
645                                //won't be thrown because we're writing to a DOM
646                        }
647                }
648
649                @Override
650                protected void _write(VCard vcard, List<VCardProperty> properties) throws IOException {
651                        //group properties by group name (null = no group name)
652                        ListMultimap<String, VCardProperty> propertiesByGroup = new ListMultimap<>();
653                        for (VCardProperty property : properties) {
654                                propertiesByGroup.put(property.getGroup(), property);
655                        }
656
657                        //marshal each property object
658                        Element vcardElement = createElement(VCARD);
659                        for (Map.Entry<String, List<VCardProperty>> entry : propertiesByGroup) {
660                                String groupName = entry.getKey();
661                                Element parent;
662                                if (groupName != null) {
663                                        Element groupElement = createElement(GROUP);
664                                        groupElement.setAttribute("name", groupName);
665                                        vcardElement.appendChild(groupElement);
666                                        parent = groupElement;
667                                } else {
668                                        parent = vcardElement;
669                                }
670
671                                for (VCardProperty property : entry.getValue()) {
672                                        try {
673                                                Element propertyElement = marshalProperty(property, vcard);
674                                                parent.appendChild(propertyElement);
675                                        } catch (SkipMeException e) {
676                                                //skip property
677                                        } catch (EmbeddedVCardException e) {
678                                                //skip property
679                                        }
680                                }
681                        }
682
683                        if (vcardsRootElement == null) {
684                                vcardsRootElement = createElement(VCARDS);
685                                Element documentRoot = document.getDocumentElement();
686                                if (documentRoot == null) {
687                                        document.appendChild(vcardsRootElement);
688                                } else {
689                                        documentRoot.appendChild(vcardsRootElement);
690                                }
691                        }
692                        vcardsRootElement.appendChild(vcardElement);
693                }
694
695                public void close() {
696                        //empty
697                }
698
699                /**
700                 * Marshals a type object to an XML element.
701                 * @param property the property to marshal
702                 * @param vcard the vcard the type belongs to
703                 * @return the XML element
704                 */
705                @SuppressWarnings({ "rawtypes", "unchecked" })
706                private Element marshalProperty(VCardProperty property, VCard vcard) {
707                        VCardPropertyScribe scribe = index.getPropertyScribe(property);
708
709                        Element propertyElement;
710                        if (property instanceof Xml) {
711                                Xml xml = (Xml) property;
712                                Document propertyDocument = xml.getValue();
713                                if (propertyDocument == null) {
714                                        throw new SkipMeException();
715                                }
716                                propertyElement = propertyDocument.getDocumentElement();
717                                propertyElement = (Element) document.importNode(propertyElement, true);
718                        } else {
719                                QName qname = scribe.getQName();
720                                propertyElement = createElement(qname);
721                                scribe.writeXml(property, propertyElement);
722                        }
723
724                        //marshal the parameters
725                        VCardParameters parameters = scribe.prepareParameters(property, targetVersion, vcard);
726                        removeUnsupportedParameters(parameters);
727                        if (!parameters.isEmpty()) {
728                                Element parametersElement = marshalParameters(parameters);
729                                Node firstChild = propertyElement.getFirstChild();
730                                propertyElement.insertBefore(parametersElement, firstChild);
731                        }
732
733                        return propertyElement;
734                }
735
736                private Element marshalParameters(VCardParameters parameters) {
737                        Element parametersElement = createElement(PARAMETERS);
738
739                        for (Map.Entry<String, List<String>> parameter : parameters) {
740                                String parameterName = parameter.getKey().toLowerCase();
741                                Element parameterElement = createElement(parameterName);
742
743                                for (String parameterValue : parameter.getValue()) {
744                                        VCardDataType dataType = parameterDataTypes.get(parameterName);
745                                        String dataTypeElementName = (dataType == null) ? "unknown" : dataType.getName().toLowerCase();
746                                        Element dataTypeElement = createElement(dataTypeElementName);
747                                        dataTypeElement.setTextContent(parameterValue);
748                                        parameterElement.appendChild(dataTypeElement);
749                                }
750
751                                parametersElement.appendChild(parameterElement);
752                        }
753
754                        return parametersElement;
755                }
756
757                /**
758                 * Creates a new XML element under the vCard namespace.
759                 * @param name the name of the XML element
760                 * @return the new XML element
761                 */
762                private Element createElement(String name) {
763                        return createElement(name, targetVersion.getXmlNamespace());
764                }
765
766                /**
767                 * Creates a new XML element.
768                 * @param name the name of the XML element
769                 * @param ns the namespace of the XML element
770                 * @return the new XML element
771                 */
772                private Element createElement(String name, String ns) {
773                        return document.createElementNS(ns, name);
774                }
775
776                /**
777                 * Creates a new XML element.
778                 * @param qname the element name
779                 * @return the new XML element
780                 */
781                private Element createElement(QName qname) {
782                        return createElement(qname.getLocalPart(), qname.getNamespaceURI());
783                }
784        }
785}