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.Collections;
020import java.util.Iterator;
021import java.util.List;
022import java.util.Map;
023import java.util.stream.Collectors;
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-2026, 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                        TransformerFactory factory = TransformerFactory.newInstance();
472                        XmlUtils.applyXXEProtection(factory);
473                        transformer = factory.newTransformer();
474                } catch (TransformerConfigurationException | TransformerFactoryConfigurationError e) {
475                        //should never be thrown because we're not doing anything fancy with the configuration
476                        throw new RuntimeException(e);
477                }
478
479                /*
480                 * Using Transformer#setOutputProperties(Properties) doesn't work for
481                 * some reason for setting the number of indentation spaces.
482                 */
483                outputProperties.forEach(transformer::setOutputProperty);
484
485                DOMSource source = new DOMSource(document);
486                StreamResult result = new StreamResult(writer);
487                transformer.transform(source, result);
488        }
489
490        private class XCardDocumentStreamReader extends StreamReader {
491                private final Iterator<Element> vcardElements;
492
493                private VCard vcard;
494
495                public XCardDocumentStreamReader() {
496                        List<Element> list = (vcardsRootElement == null) ? Collections.<Element> emptyList() : getChildElements(vcardsRootElement, VCARD);
497                        vcardElements = list.iterator();
498                }
499
500                @Override
501                public VCard readNext() {
502                        try {
503                                return super.readNext();
504                        } catch (IOException e) {
505                                //will not be thrown
506                                throw new UncheckedIOException(e);
507                        }
508                }
509
510                @Override
511                protected VCard _readNext() throws IOException {
512                        if (!vcardElements.hasNext()) {
513                                return null;
514                        }
515
516                        vcard = new VCard();
517                        vcard.setVersion(version4);
518                        context.setVersion(version4);
519                        parseVCardElement(vcardElements.next());
520                        return vcard;
521                }
522
523                public void close() {
524                        //empty
525                }
526
527                private void parseVCardElement(Element vcardElement) {
528                        List<Element> children = XmlUtils.toElementList(vcardElement.getChildNodes());
529                        for (Element child : children) {
530                                if (XmlUtils.hasQName(child, GROUP)) {
531                                        String group = child.getAttribute("name");
532                                        if (group.isEmpty()) {
533                                                group = null;
534                                        }
535                                        List<Element> grandChildren = XmlUtils.toElementList(child.getChildNodes());
536                                        for (Element grandChild : grandChildren) {
537                                                parseAndAddElement(grandChild, group);
538                                        }
539                                        continue;
540                                }
541
542                                parseAndAddElement(child, null);
543                        }
544                }
545
546                /**
547                 * Parses a property element from the XML document and adds the property
548                 * to the vCard.
549                 * @param element the element to parse
550                 * @param group the group name or null if the property does not belong
551                 * to a group
552                 */
553                private void parseAndAddElement(Element element, String group) {
554                        VCardParameters parameters = parseParameters(element);
555
556                        VCardProperty property;
557                        String propertyName = element.getLocalName();
558                        String ns = element.getNamespaceURI();
559                        QName qname = new QName(ns, propertyName);
560                        VCardPropertyScribe<? extends VCardProperty> scribe = index.getPropertyScribe(qname);
561
562                        context.getWarnings().clear();
563                        context.setPropertyName(propertyName);
564                        try {
565                                property = scribe.parseXml(element, parameters, context);
566                                property.setGroup(group);
567                                warnings.addAll(context.getWarnings());
568                        } catch (SkipMeException e) {
569                                //@formatter:off
570                                warnings.add(new ParseWarning.Builder(context)
571                                        .message(22, e.getMessage())
572                                        .build()
573                                );
574                                //@formatter:on
575                                return;
576                        } catch (CannotParseException e) {
577                                //@formatter:off
578                                warnings.add(new ParseWarning.Builder(context)
579                                        .message(e)
580                                        .build()
581                                );
582                                //@formatter:on
583
584                                scribe = index.getPropertyScribe(Xml.class);
585                                property = scribe.parseXml(element, parameters, context);
586                                property.setGroup(group);
587                        } catch (EmbeddedVCardException e) {
588                                //@formatter:off
589                                warnings.add(new ParseWarning.Builder(context)
590                                        .message(34)
591                                        .build()
592                                );
593                                //@formatter:on
594                                return;
595                        }
596
597                        vcard.addProperty(property);
598                }
599
600                /**
601                 * Parses the property parameters.
602                 * @param element the property's XML element
603                 * @return the parsed parameters
604                 */
605                private VCardParameters parseParameters(Element element) {
606                        VCardParameters parameters = new VCardParameters();
607
608                        List<Element> roots = XmlUtils.toElementList(element.getElementsByTagNameNS(PARAMETERS.getNamespaceURI(), PARAMETERS.getLocalPart()));
609                        for (Element root : roots) { // foreach "<parameters>" element (there should only be 1 though)
610                                List<Element> parameterElements = XmlUtils.toElementList(root.getChildNodes());
611                                for (Element parameterElement : parameterElements) {
612                                        String name = parameterElement.getLocalName().toUpperCase();
613                                        List<Element> valueElements = XmlUtils.toElementList(parameterElement.getChildNodes());
614                                        for (Element valueElement : valueElements) {
615                                                String value = valueElement.getTextContent();
616                                                parameters.put(name, value);
617                                        }
618                                }
619                        }
620
621                        return parameters;
622                }
623
624                private List<Element> getChildElements(Element parent, QName qname) {
625                        //@formatter:off
626                        return XmlUtils.toElementStream(parent.getChildNodes())
627                                .filter(child -> XmlUtils.hasQName(child, qname))
628                        .collect(Collectors.toList());
629                        //@formatter:on
630                }
631        }
632
633        public class XCardDocumentStreamWriter extends XCardWriterBase {
634                @Override
635                public void write(VCard vcard) {
636                        try {
637                                super.write(vcard);
638                        } catch (IOException ignore) {
639                                //won't be thrown because we're writing to a DOM
640                        }
641                }
642
643                @Override
644                protected void _write(VCard vcard, List<VCardProperty> properties) throws IOException {
645                        //group properties by group name (null = no group name)
646                        ListMultimap<String, VCardProperty> propertiesByGroup = new ListMultimap<>();
647                        properties.forEach(property -> propertiesByGroup.put(property.getGroup(), property));
648
649                        //marshal each property object
650                        Element vcardElement = createElement(VCARD);
651                        for (Map.Entry<String, List<VCardProperty>> entry : propertiesByGroup) {
652                                String groupName = entry.getKey();
653                                Element parent;
654                                if (groupName != null) {
655                                        Element groupElement = createElement(GROUP);
656                                        groupElement.setAttribute("name", groupName);
657                                        vcardElement.appendChild(groupElement);
658                                        parent = groupElement;
659                                } else {
660                                        parent = vcardElement;
661                                }
662
663                                for (VCardProperty property : entry.getValue()) {
664                                        try {
665                                                Element propertyElement = marshalProperty(property, vcard);
666                                                parent.appendChild(propertyElement);
667                                        } catch (SkipMeException | EmbeddedVCardException e) {
668                                                //skip property
669                                        }
670                                }
671                        }
672
673                        if (vcardsRootElement == null) {
674                                vcardsRootElement = createElement(VCARDS);
675                                Element documentRoot = document.getDocumentElement();
676                                if (documentRoot == null) {
677                                        document.appendChild(vcardsRootElement);
678                                } else {
679                                        documentRoot.appendChild(vcardsRootElement);
680                                }
681                        }
682                        vcardsRootElement.appendChild(vcardElement);
683                }
684
685                public void close() {
686                        //empty
687                }
688
689                /**
690                 * Marshals a type object to an XML element.
691                 * @param property the property to marshal
692                 * @param vcard the vcard the type belongs to
693                 * @return the XML element
694                 */
695                @SuppressWarnings({ "rawtypes", "unchecked" })
696                private Element marshalProperty(VCardProperty property, VCard vcard) {
697                        VCardPropertyScribe scribe = index.getPropertyScribe(property);
698
699                        Element propertyElement;
700                        if (property instanceof Xml) {
701                                Xml xml = (Xml) property;
702                                Document propertyDocument = xml.getValue();
703                                if (propertyDocument == null) {
704                                        throw new SkipMeException();
705                                }
706                                propertyElement = propertyDocument.getDocumentElement();
707                                propertyElement = (Element) document.importNode(propertyElement, true);
708                        } else {
709                                QName qname = scribe.getQName();
710                                propertyElement = createElement(qname);
711                                scribe.writeXml(property, propertyElement);
712                        }
713
714                        //marshal the parameters
715                        VCardParameters parameters = scribe.prepareParameters(property, targetVersion, vcard);
716                        removeUnsupportedParameters(parameters);
717                        if (!parameters.isEmpty()) {
718                                Element parametersElement = marshalParameters(parameters);
719                                Node firstChild = propertyElement.getFirstChild();
720                                propertyElement.insertBefore(parametersElement, firstChild);
721                        }
722
723                        return propertyElement;
724                }
725
726                private Element marshalParameters(VCardParameters parameters) {
727                        Element parametersElement = createElement(PARAMETERS);
728
729                        for (Map.Entry<String, List<String>> parameter : parameters) {
730                                String parameterName = parameter.getKey().toLowerCase();
731                                Element parameterElement = createElement(parameterName);
732                                VCardDataType dataType = parameterDataTypes.get(parameterName);
733                                String dataTypeElementName = (dataType == null) ? "unknown" : dataType.getName().toLowerCase();
734
735                                for (String parameterValue : parameter.getValue()) {
736                                        Element dataTypeElement = createElement(dataTypeElementName);
737                                        dataTypeElement.setTextContent(parameterValue);
738                                        parameterElement.appendChild(dataTypeElement);
739                                }
740
741                                parametersElement.appendChild(parameterElement);
742                        }
743
744                        return parametersElement;
745                }
746
747                /**
748                 * Creates a new XML element under the vCard namespace.
749                 * @param name the name of the XML element
750                 * @return the new XML element
751                 */
752                private Element createElement(String name) {
753                        return createElement(name, targetVersion.getXmlNamespace());
754                }
755
756                /**
757                 * Creates a new XML element.
758                 * @param name the name of the XML element
759                 * @param ns the namespace of the XML element
760                 * @return the new XML element
761                 */
762                private Element createElement(String name, String ns) {
763                        return document.createElementNS(ns, name);
764                }
765
766                /**
767                 * Creates a new XML element.
768                 * @param qname the element name
769                 * @return the new XML element
770                 */
771                private Element createElement(QName qname) {
772                        return createElement(qname.getLocalPart(), qname.getNamespaceURI());
773                }
774        }
775}