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