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