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.OutputStream;
010import java.io.OutputStreamWriter;
011import java.io.Writer;
012import java.nio.charset.StandardCharsets;
013import java.nio.file.Files;
014import java.nio.file.Path;
015import java.util.Collections;
016import java.util.List;
017import java.util.Map;
018
019import javax.xml.namespace.QName;
020import javax.xml.transform.Result;
021import javax.xml.transform.Transformer;
022import javax.xml.transform.TransformerConfigurationException;
023import javax.xml.transform.TransformerFactory;
024import javax.xml.transform.dom.DOMResult;
025import javax.xml.transform.sax.SAXTransformerFactory;
026import javax.xml.transform.sax.TransformerHandler;
027import javax.xml.transform.stream.StreamResult;
028
029import org.w3c.dom.Document;
030import org.w3c.dom.Element;
031import org.w3c.dom.NamedNodeMap;
032import org.w3c.dom.Node;
033import org.w3c.dom.NodeList;
034import org.w3c.dom.Text;
035import org.xml.sax.Attributes;
036import org.xml.sax.SAXException;
037import org.xml.sax.helpers.AttributesImpl;
038
039import ezvcard.VCard;
040import ezvcard.VCardDataType;
041import ezvcard.io.EmbeddedVCardException;
042import ezvcard.io.SkipMeException;
043import ezvcard.io.scribe.VCardPropertyScribe;
044import ezvcard.parameter.VCardParameters;
045import ezvcard.property.VCardProperty;
046import ezvcard.property.Xml;
047import ezvcard.util.ListMultimap;
048import ezvcard.util.XmlUtils;
049
050/*
051 Copyright (c) 2012-2026, Michael Angstadt
052 All rights reserved.
053
054 Redistribution and use in source and binary forms, with or without
055 modification, are permitted provided that the following conditions are met: 
056
057 1. Redistributions of source code must retain the above copyright notice, this
058 list of conditions and the following disclaimer. 
059 2. Redistributions in binary form must reproduce the above copyright notice,
060 this list of conditions and the following disclaimer in the documentation
061 and/or other materials provided with the distribution. 
062
063 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
064 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
065 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
066 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
067 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
068 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
069 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
070 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
071 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
072 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
073
074 The views and conclusions contained in the software and documentation are those
075 of the authors and should not be interpreted as representing official policies, 
076 either expressed or implied, of the FreeBSD Project.
077 */
078
079/**
080 * <p>
081 * Writes xCards (XML-encoded vCards) in a streaming fashion.
082 * </p>
083 * <p>
084 * <b>Example:</b>
085 * </p>
086 * 
087 * <pre class="brush:java">
088 * VCard vcard1 = ...
089 * VCard vcard2 = ...
090 * Path file = Paths.get("vcards.xml");
091 * try (XCardWriter writer = new XCardWriter(file)) {
092 *   writer.write(vcard1);
093 *   writer.write(vcard2);
094 * }
095 * </pre>
096 * @author Michael Angstadt
097 * @see <a href="http://tools.ietf.org/html/rfc6351">RFC 6351</a>
098 */
099public class XCardWriter extends XCardWriterBase {
100        //How to use SAX to write XML: http://stackoverflow.com/q/4898590
101
102        private final Document DOC = XmlUtils.createDocument();
103
104        private final Writer writer;
105        private final TransformerHandler handler;
106        private final boolean vcardsElementExists;
107        private boolean started = false;
108
109        /**
110         * @param out the output stream to write to (UTF-8 encoding will be used)
111         */
112        public XCardWriter(OutputStream out) {
113                this(out, (Integer) null);
114        }
115
116        /**
117         * @param out the output stream to write to (UTF-8 encoding will be used)
118         * @param indent the number of indent spaces to use for pretty-printing or
119         * null to disable pretty-printing (disabled by default)
120         */
121        public XCardWriter(OutputStream out, Integer indent) {
122                this(out, indent, null);
123        }
124
125        /**
126         * @param out the output stream to write to (UTF-8 encoding will be used)
127         * @param indent the number of indent spaces to use for pretty-printing or
128         * null to disable pretty-printing (disabled by default)
129         * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many
130         * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library
131         * like <a href=
132         * "http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22xalan%22%20AND%20a%3A%22xalan%22"
133         * >xalan</a> to your project)
134         */
135        public XCardWriter(OutputStream out, Integer indent, String xmlVersion) {
136                this(out, new XCardOutputProperties(indent, xmlVersion));
137        }
138
139        /**
140         * @param out the output stream to write to (UTF-8 encoding will be used)
141         * @param outputProperties properties to assign to the JAXP transformer (see
142         * {@link Transformer#setOutputProperty})
143         */
144        public XCardWriter(OutputStream out, Map<String, String> outputProperties) {
145                this(new OutputStreamWriter(out, StandardCharsets.UTF_8), outputProperties);
146        }
147
148        /**
149         * @param file the file to write to (UTF-8 encoding will be used)
150         * @throws IOException if there is a problem opening the file
151         */
152        public XCardWriter(Path file) throws IOException {
153                this(file, (Integer) null);
154        }
155
156        /**
157         * @param file the file to write to (UTF-8 encoding will be used)
158         * @param indent the number of indent spaces to use for pretty-printing or
159         * null to disable pretty-printing (disabled by default)
160         * @throws IOException if there is a problem opening the file
161         */
162        public XCardWriter(Path file, Integer indent) throws IOException {
163                this(file, indent, null);
164        }
165
166        /**
167         * @param file the file to write to (UTF-8 encoding will be used)
168         * @param indent the number of indent spaces to use for pretty-printing or
169         * null to disable pretty-printing (disabled by default)
170         * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many
171         * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library
172         * like <a href=
173         * "http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22xalan%22%20AND%20a%3A%22xalan%22"
174         * >xalan</a> to your project)
175         * @throws IOException if there is a problem opening the file
176         */
177        public XCardWriter(Path file, Integer indent, String xmlVersion) throws IOException {
178                this(file, new XCardOutputProperties(indent, xmlVersion));
179        }
180
181        /**
182         * @param file the file to write to (UTF-8 encoding will be used)
183         * @param outputProperties properties to assign to the JAXP transformer (see
184         * {@link Transformer#setOutputProperty})
185         * @throws IOException if there is a problem opening the file
186         */
187        public XCardWriter(Path file, Map<String, String> outputProperties) throws IOException {
188                this(Files.newBufferedWriter(file, StandardCharsets.UTF_8), outputProperties);
189        }
190
191        /**
192         * @param writer the writer to write to
193         */
194        public XCardWriter(Writer writer) {
195                this(writer, (Integer) null);
196        }
197
198        /**
199         * @param writer the writer to write to
200         * @param indent the number of indent spaces to use for pretty-printing or
201         * null to disable pretty-printing (disabled by default)
202         */
203        public XCardWriter(Writer writer, Integer indent) {
204                this(writer, indent, null);
205        }
206
207        /**
208         * @param writer the writer to write to
209         * @param indent the number of indent spaces to use for pretty-printing or
210         * null to disable pretty-printing (disabled by default)
211         * @param xmlVersion the XML version to use (defaults to "1.0") (Note: Many
212         * JDKs only support 1.0 natively. For XML 1.1 support, add a JAXP library
213         * like <a href=
214         * "http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22xalan%22%20AND%20a%3A%22xalan%22"
215         * >xalan</a> to your project)
216         */
217        public XCardWriter(Writer writer, Integer indent, String xmlVersion) {
218                this(writer, new XCardOutputProperties(indent, xmlVersion));
219        }
220
221        /**
222         * @param writer the writer to write to
223         * @param outputProperties properties to assign to the JAXP transformer (see
224         * {@link Transformer#setOutputProperty})
225         */
226        public XCardWriter(Writer writer, Map<String, String> outputProperties) {
227                this(writer, null, outputProperties);
228        }
229
230        /**
231         * @param parent the DOM node to add child elements to
232         */
233        public XCardWriter(Node parent) {
234                this(null, parent, Collections.<String, String> emptyMap());
235        }
236
237        private XCardWriter(Writer writer, Node parent, Map<String, String> outputProperties) {
238                this.writer = writer;
239
240                if (parent instanceof Document) {
241                        Node root = ((Document) parent).getDocumentElement();
242                        if (root != null) {
243                                parent = root;
244                        }
245                }
246                this.vcardsElementExists = isVCardsElement(parent);
247
248                try {
249                        SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance();
250                        handler = factory.newTransformerHandler();
251                } catch (TransformerConfigurationException e) {
252                        throw new RuntimeException(e);
253                }
254
255                Transformer transformer = handler.getTransformer();
256
257                /*
258                 * Using Transformer#setOutputProperties(Properties) doesn't work for
259                 * some reason for setting the number of indentation spaces.
260                 */
261                outputProperties.forEach(transformer::setOutputProperty);
262
263                Result result = (writer == null) ? new DOMResult(parent) : new StreamResult(writer);
264                handler.setResult(result);
265        }
266
267        private boolean isVCardsElement(Node node) {
268                if (node == null) {
269                        return false;
270                }
271
272                if (!(node instanceof Element)) {
273                        return false;
274                }
275
276                return XmlUtils.hasQName(node, VCARDS);
277        }
278
279        @Override
280        protected void _write(VCard vcard, List<VCardProperty> properties) throws IOException {
281                try {
282                        if (!started) {
283                                handler.startDocument();
284
285                                if (!vcardsElementExists) {
286                                        //don't output a <vcards> element if the parent is a <vcards> element
287                                        start(VCARDS);
288                                }
289
290                                started = true;
291                        }
292
293                        ListMultimap<String, VCardProperty> propertiesByGroup = new ListMultimap<>(); //group the types by group name (null = no group name)
294                        properties.forEach(property -> propertiesByGroup.put(property.getGroup(), property));
295
296                        start(VCARD);
297
298                        for (Map.Entry<String, List<VCardProperty>> entry : propertiesByGroup) {
299                                String groupName = entry.getKey();
300                                if (groupName != null) {
301                                        AttributesImpl attr = new AttributesImpl();
302                                        attr.addAttribute(XCardQNames.NAMESPACE, "", "name", "", groupName);
303
304                                        start(GROUP, attr);
305                                }
306
307                                for (VCardProperty property : entry.getValue()) {
308                                        write(property, vcard);
309                                }
310
311                                if (groupName != null) {
312                                        end(GROUP);
313                                }
314                        }
315
316                        end(VCARD);
317                } catch (SAXException e) {
318                        throw new IOException(e);
319                }
320        }
321
322        /**
323         * Terminates the XML document and closes the output stream.
324         */
325        public void close() throws IOException {
326                try {
327                        if (!started) {
328                                handler.startDocument();
329
330                                if (!vcardsElementExists) {
331                                        //don't output a <vcards> element if the parent is a <vcards> element
332                                        start(VCARDS);
333                                }
334                        }
335
336                        if (!vcardsElementExists) {
337                                end(VCARDS);
338                        }
339                        handler.endDocument();
340                } catch (SAXException e) {
341                        throw new IOException(e);
342                }
343
344                if (writer != null) {
345                        writer.close();
346                }
347        }
348
349        @SuppressWarnings({ "rawtypes", "unchecked" })
350        private void write(VCardProperty property, VCard vcard) throws SAXException {
351                VCardPropertyScribe scribe = index.getPropertyScribe(property);
352                VCardParameters parameters = scribe.prepareParameters(property, targetVersion, vcard);
353
354                removeUnsupportedParameters(parameters);
355
356                //get the property element to write
357                Element propertyElement;
358                if (property instanceof Xml) {
359                        Xml xml = (Xml) property;
360                        Document value = xml.getValue();
361                        if (value == null) {
362                                return;
363                        }
364                        propertyElement = value.getDocumentElement();
365                } else {
366                        QName qname = scribe.getQName();
367                        propertyElement = DOC.createElementNS(qname.getNamespaceURI(), qname.getLocalPart());
368                        try {
369                                scribe.writeXml(property, propertyElement);
370                        } catch (SkipMeException | EmbeddedVCardException e) {
371                                return;
372                        }
373                }
374
375                start(propertyElement);
376
377                write(parameters);
378                write(propertyElement);
379
380                end(propertyElement);
381        }
382
383        private void write(Element propertyElement) throws SAXException {
384                NodeList children = propertyElement.getChildNodes();
385                for (Node child : XmlUtils.iterable(children)) {
386                        if (child instanceof Element) {
387                                Element element = (Element) child;
388
389                                if (element.hasChildNodes()) {
390                                        start(element);
391                                        write(element);
392                                        end(element);
393                                } else {
394                                        childless(element);
395                                }
396
397                                continue;
398                        }
399
400                        if (child instanceof Text) {
401                                Text text = (Text) child;
402                                text(text.getTextContent());
403                                continue;
404                        }
405                }
406        }
407
408        private void write(VCardParameters parameters) throws SAXException {
409                if (parameters.isEmpty()) {
410                        return;
411                }
412
413                start(PARAMETERS);
414
415                for (Map.Entry<String, List<String>> parameter : parameters) {
416                        String parameterName = parameter.getKey().toLowerCase();
417                        start(parameterName);
418
419                        for (String parameterValue : parameter.getValue()) {
420                                VCardDataType dataType = parameterDataTypes.get(parameterName);
421                                String dataTypeElementName = (dataType == null) ? "unknown" : dataType.getName().toLowerCase();
422
423                                start(dataTypeElementName);
424                                text(parameterValue);
425                                end(dataTypeElementName);
426                        }
427
428                        end(parameterName);
429                }
430
431                end(PARAMETERS);
432        }
433
434        /**
435         * Makes an childless element appear as {@code <foo />} instead of
436         * {@code <foo></foo>}
437         * @param element the element
438         * @throws SAXException if there is a problem creating the element
439         */
440        private void childless(Element element) throws SAXException {
441                Attributes attributes = getElementAttributes(element);
442                handler.startElement(element.getNamespaceURI(), "", element.getLocalName(), attributes);
443                handler.endElement(element.getNamespaceURI(), "", element.getLocalName());
444        }
445
446        private void start(Element element) throws SAXException {
447                Attributes attributes = getElementAttributes(element);
448                start(element.getNamespaceURI(), element.getLocalName(), attributes);
449        }
450
451        private void start(String element) throws SAXException {
452                start(element, new AttributesImpl());
453        }
454
455        private void start(QName qname) throws SAXException {
456                start(qname, new AttributesImpl());
457        }
458
459        private void start(QName qname, Attributes attributes) throws SAXException {
460                start(qname.getNamespaceURI(), qname.getLocalPart(), attributes);
461        }
462
463        private void start(String element, Attributes attributes) throws SAXException {
464                start(targetVersion.getXmlNamespace(), element, attributes);
465        }
466
467        private void start(String namespace, String element, Attributes attributes) throws SAXException {
468                handler.startElement(namespace, "", element, attributes);
469        }
470
471        private void end(Element element) throws SAXException {
472                end(element.getNamespaceURI(), element.getLocalName());
473        }
474
475        private void end(String element) throws SAXException {
476                end(targetVersion.getXmlNamespace(), element);
477        }
478
479        private void end(QName qname) throws SAXException {
480                end(qname.getNamespaceURI(), qname.getLocalPart());
481        }
482
483        private void end(String namespace, String element) throws SAXException {
484                handler.endElement(namespace, "", element);
485        }
486
487        private void text(String text) throws SAXException {
488                handler.characters(text.toCharArray(), 0, text.length());
489        }
490
491        private Attributes getElementAttributes(Element element) {
492                AttributesImpl attributes = new AttributesImpl();
493                NamedNodeMap attributeNodes = element.getAttributes();
494
495                //@formatter:off
496                XmlUtils.stream(attributeNodes)
497                        .filter(node -> !"xmlns".equals(node.getLocalName()))
498                .forEach(node -> attributes.addAttribute(node.getNamespaceURI(), "", node.getLocalName(), "", node.getNodeValue()));
499                //@formatter:on
500
501                return attributes;
502        }
503}