001package biweekly.io.xml;
002
003import static biweekly.io.xml.XCalNamespaceContext.XCAL_NS;
004import static biweekly.io.xml.XCalQNames.COMPONENTS;
005import static biweekly.io.xml.XCalQNames.ICALENDAR;
006import static biweekly.io.xml.XCalQNames.PARAMETERS;
007import static biweekly.io.xml.XCalQNames.PROPERTIES;
008import static biweekly.util.IOUtils.utf8Writer;
009import static biweekly.util.StringUtils.NEWLINE;
010
011import java.io.Closeable;
012import java.io.File;
013import java.io.IOException;
014import java.io.OutputStream;
015import java.io.Writer;
016import java.util.Collection;
017import java.util.HashMap;
018import java.util.List;
019import java.util.Map;
020
021import javax.xml.namespace.QName;
022import javax.xml.transform.Result;
023import javax.xml.transform.TransformerConfigurationException;
024import javax.xml.transform.TransformerFactory;
025import javax.xml.transform.dom.DOMResult;
026import javax.xml.transform.sax.SAXTransformerFactory;
027import javax.xml.transform.sax.TransformerHandler;
028import javax.xml.transform.stream.StreamResult;
029
030import org.w3c.dom.Document;
031import org.w3c.dom.Element;
032import org.w3c.dom.NamedNodeMap;
033import org.w3c.dom.Node;
034import org.w3c.dom.NodeList;
035import org.w3c.dom.Text;
036import org.xml.sax.Attributes;
037import org.xml.sax.SAXException;
038import org.xml.sax.helpers.AttributesImpl;
039
040import biweekly.ICalDataType;
041import biweekly.ICalendar;
042import biweekly.component.ICalComponent;
043import biweekly.io.SkipMeException;
044import biweekly.io.scribe.ScribeIndex;
045import biweekly.io.scribe.component.ICalComponentScribe;
046import biweekly.io.scribe.property.ICalPropertyScribe;
047import biweekly.parameter.ICalParameters;
048import biweekly.property.ICalProperty;
049import biweekly.property.Xml;
050import biweekly.util.XmlUtils;
051
052/*
053 Copyright (c) 2013, Michael Angstadt
054 All rights reserved.
055
056 Redistribution and use in source and binary forms, with or without
057 modification, are permitted provided that the following conditions are met: 
058
059 1. Redistributions of source code must retain the above copyright notice, this
060 list of conditions and the following disclaimer. 
061 2. Redistributions in binary form must reproduce the above copyright notice,
062 this list of conditions and the following disclaimer in the documentation
063 and/or other materials provided with the distribution. 
064
065 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
066 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
067 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
068 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
069 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
070 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
071 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
072 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
073 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
074 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
075
076 The views and conclusions contained in the software and documentation are those
077 of the authors and should not be interpreted as representing official policies, 
078 either expressed or implied, of the FreeBSD Project.
079 */
080
081/**
082 * <p>
083 * Writes xCards (XML-encoded vCards) in a streaming fashion.
084 * </p>
085 * <p>
086 * <b>Example:</b>
087 * 
088 * <pre class="brush:java">
089 * VCard vcard1 = ...
090 * VCard vcard2 = ...
091 * 
092 * File file = new File("vcards.xml");
093 * XCardWriter xcardWriter = new XCardWriter(file);
094 * xcardWriter.write(vcard1);
095 * xcardWriter.write(vcard2);
096 * xcardWriter.close();
097 * </pre>
098 * @author Michael Angstadt
099 * @see <a href="http://tools.ietf.org/html/rfc6351">RFC 6351</a>
100 */
101public class XCalWriter implements Closeable {
102        //how to use SAX to write XML: http://stackoverflow.com/questions/4898590/generating-xml-using-sax-and-java
103        private final Document DOC = XmlUtils.createDocument();
104
105        /**
106         * Defines the names of the XML elements that are used to hold each
107         * parameter's value.
108         */
109        private final Map<String, ICalDataType> parameterDataTypes = new HashMap<String, ICalDataType>();
110        {
111                registerParameterDataType(ICalParameters.CN, ICalDataType.TEXT);
112                registerParameterDataType(ICalParameters.ALTREP, ICalDataType.URI);
113                registerParameterDataType(ICalParameters.CUTYPE, ICalDataType.TEXT);
114                registerParameterDataType(ICalParameters.DELEGATED_FROM, ICalDataType.CAL_ADDRESS);
115                registerParameterDataType(ICalParameters.DELEGATED_TO, ICalDataType.CAL_ADDRESS);
116                registerParameterDataType(ICalParameters.DIR, ICalDataType.URI);
117                registerParameterDataType(ICalParameters.ENCODING, ICalDataType.TEXT);
118                registerParameterDataType(ICalParameters.FMTTYPE, ICalDataType.TEXT);
119                registerParameterDataType(ICalParameters.FBTYPE, ICalDataType.TEXT);
120                registerParameterDataType(ICalParameters.LANGUAGE, ICalDataType.TEXT);
121                registerParameterDataType(ICalParameters.MEMBER, ICalDataType.CAL_ADDRESS);
122                registerParameterDataType(ICalParameters.PARTSTAT, ICalDataType.TEXT);
123                registerParameterDataType(ICalParameters.RANGE, ICalDataType.TEXT);
124                registerParameterDataType(ICalParameters.RELATED, ICalDataType.TEXT);
125                registerParameterDataType(ICalParameters.RELTYPE, ICalDataType.TEXT);
126                registerParameterDataType(ICalParameters.ROLE, ICalDataType.TEXT);
127                registerParameterDataType(ICalParameters.RSVP, ICalDataType.BOOLEAN);
128                registerParameterDataType(ICalParameters.SENT_BY, ICalDataType.CAL_ADDRESS);
129                registerParameterDataType(ICalParameters.TZID, ICalDataType.TEXT);
130        }
131
132        private final Writer writer;
133        private final TransformerHandler handler;
134        private final String indent;
135        private final boolean icalendarElementExists;
136        private int level = 0;
137        private boolean textNodeJustPrinted = false, started = false;
138        private ScribeIndex index = new ScribeIndex();
139
140        /**
141         * Creates an xCard writer (UTF-8 encoding will be used).
142         * @param out the output stream to write the xCards to
143         */
144        public XCalWriter(OutputStream out) {
145                this(utf8Writer(out));
146        }
147
148        /**
149         * Creates an xCard writer (UTF-8 encoding will be used).
150         * @param out the output stream to write the xCards to
151         * @param indent the indentation string to use for pretty printing (e.g.
152         * "\t") or null not to pretty print
153         */
154        public XCalWriter(OutputStream out, String indent) {
155                this(utf8Writer(out), indent);
156        }
157
158        /**
159         * Creates an xCard writer (UTF-8 encoding will be used).
160         * @param file the file to write the xCards to
161         * @throws IOException if there's a problem opening the file
162         */
163        public XCalWriter(File file) throws IOException {
164                this(utf8Writer(file));
165        }
166
167        /**
168         * Creates an xCard writer (UTF-8 encoding will be used).
169         * @param file the file to write the xCards to
170         * @param indent the indentation string to use for pretty printing (e.g.
171         * "\t") or null not to pretty print
172         * @throws IOException if there's a problem opening the file
173         */
174        public XCalWriter(File file, String indent) throws IOException {
175                this(utf8Writer(file), indent);
176        }
177
178        /**
179         * Creates an xCard writer.
180         * @param writer the writer to write to
181         */
182        public XCalWriter(Writer writer) {
183                this(writer, null);
184        }
185
186        /**
187         * Creates an xCard writer.
188         * @param writer the writer to write to
189         * @param indent the indentation string to use for pretty printing (e.g.
190         * "\t") or null not to pretty print
191         */
192        public XCalWriter(Writer writer, String indent) {
193                this(writer, indent, null);
194        }
195
196        /**
197         * Creates an xCard writer.
198         * @param parent the DOM node to add child elements to
199         */
200        public XCalWriter(Node parent) {
201                this(null, null, parent);
202        }
203
204        private XCalWriter(Writer writer, String indent, Node parent) {
205                this.writer = writer;
206                this.indent = indent;
207
208                if (parent instanceof Document) {
209                        Node root = parent.getFirstChild();
210                        if (root != null) {
211                                parent = root;
212                        }
213                }
214                this.icalendarElementExists = isICalendarElement(parent);
215
216                try {
217                        SAXTransformerFactory factory = (SAXTransformerFactory) TransformerFactory.newInstance();
218                        handler = factory.newTransformerHandler();
219                } catch (TransformerConfigurationException e) {
220                        throw new RuntimeException(e);
221                }
222
223                Result result = (writer == null) ? new DOMResult(parent) : new StreamResult(writer);
224                handler.setResult(result);
225        }
226
227        private boolean isICalendarElement(Node node) {
228                if (node == null) {
229                        return false;
230                }
231
232                if (!(node instanceof Element)) {
233                        return false;
234                }
235
236                return ICALENDAR.getNamespaceURI().equals(node.getNamespaceURI()) && ICALENDAR.getLocalPart().equals(node.getLocalName());
237        }
238
239        /**
240         * <p>
241         * Registers an experimental property scribe. Can also be used to override
242         * the scribe of a standard property (such as DTSTART). Calling this method
243         * is the same as calling:
244         * </p>
245         * <p>
246         * {@code getScribeIndex().register(scribe)}.
247         * </p>
248         * @param scribe the scribe to register
249         */
250        public void registerScribe(ICalPropertyScribe<? extends ICalProperty> scribe) {
251                index.register(scribe);
252        }
253
254        /**
255         * <p>
256         * Registers an experimental component scribe. Can also be used to override
257         * the scribe of a standard component (such as VEVENT). Calling this method
258         * is the same as calling:
259         * </p>
260         * <p>
261         * {@code getScribeIndex().register(scribe)}.
262         * </p>
263         * @param scribe the scribe to register
264         */
265        public void registerScribe(ICalComponentScribe<? extends ICalComponent> scribe) {
266                index.register(scribe);
267        }
268
269        /**
270         * Gets the object that manages the component/property scribes.
271         * @return the scribe index
272         */
273        public ScribeIndex getScribeIndex() {
274                return index;
275        }
276
277        /**
278         * Sets the object that manages the component/property scribes.
279         * @param scribe the scribe index
280         */
281        public void setScribeIndex(ScribeIndex scribe) {
282                this.index = scribe;
283        }
284
285        /**
286         * Writes an iCalendar object.
287         * @param ical the iCalendar object to write
288         * @throws SAXException if there's a problem writing the iCalendar object
289         * @throws IllegalArgumentException if the scribe index is missing scribes
290         * for one or more properties/components.
291         */
292        public void write(ICalendar ical) throws SAXException {
293                index.hasScribesFor(ical);
294
295                if (!started) {
296                        handler.startDocument();
297
298                        if (!icalendarElementExists) {
299                                //don't output a <icalendar> element if the parent is a <icalendar> element
300                                start(ICALENDAR);
301                                level++;
302                        }
303
304                        started = true;
305                }
306
307                write((ICalComponent) ical);
308        }
309
310        /**
311         * Registers the data type of an experimental parameter. Experimental
312         * parameters use the "unknown" data type by default.
313         * @param parameterName the parameter name (e.g. "x-foo")
314         * @param dataType the data type or null to remove
315         */
316        public void registerParameterDataType(String parameterName, ICalDataType dataType) {
317                parameterName = parameterName.toLowerCase();
318                if (dataType == null) {
319                        parameterDataTypes.remove(parameterName);
320                } else {
321                        parameterDataTypes.put(parameterName, dataType);
322                }
323        }
324
325        @SuppressWarnings({ "rawtypes", "unchecked" })
326        private void write(ICalComponent component) throws SAXException {
327                ICalComponentScribe scribe = index.getComponentScribe(component);
328                String name = scribe.getComponentName().toLowerCase();
329
330                start(name);
331                level++;
332
333                List<ICalProperty> properties = scribe.getProperties(component);
334                if (!properties.isEmpty()) {
335                        start(PROPERTIES);
336                        level++;
337
338                        for (Object propertyObj : scribe.getProperties(component)) {
339                                ICalProperty property = (ICalProperty) propertyObj;
340                                write(property);
341                        }
342
343                        level--;
344                        end(PROPERTIES);
345                }
346
347                Collection<ICalComponent> components = scribe.getComponents(component);
348                if (!components.isEmpty()) {
349                        start(COMPONENTS);
350                        level++;
351
352                        for (Object subComponentObj : scribe.getComponents(component)) {
353                                ICalComponent subComponent = (ICalComponent) subComponentObj;
354                                write(subComponent);
355                        }
356
357                        level--;
358                        end(COMPONENTS);
359                }
360
361                level--;
362                end(name);
363        }
364
365        @SuppressWarnings({ "rawtypes", "unchecked" })
366        private void write(ICalProperty property) throws SAXException {
367                ICalPropertyScribe scribe = index.getPropertyScribe(property);
368                ICalParameters parameters = scribe.prepareParameters(property);
369
370                //get the property element to write
371                Element propertyElement;
372                if (property instanceof Xml) {
373                        Xml xml = (Xml) property;
374                        Document value = xml.getValue();
375                        if (value == null) {
376                                return;
377                        }
378                        propertyElement = XmlUtils.getRootElement(value);
379                } else {
380                        QName qname = scribe.getQName();
381                        propertyElement = DOC.createElementNS(qname.getNamespaceURI(), qname.getLocalPart());
382                        try {
383                                scribe.writeXml(property, propertyElement);
384                        } catch (SkipMeException e) {
385                                return;
386                        }
387                }
388
389                start(propertyElement);
390                level++;
391
392                write(parameters);
393                write(propertyElement);
394
395                level--;
396                end(propertyElement);
397        }
398
399        private void write(Element propertyElement) throws SAXException {
400                NodeList children = propertyElement.getChildNodes();
401                for (int i = 0; i < children.getLength(); i++) {
402                        Node child = children.item(i);
403
404                        if (child instanceof Element) {
405                                Element element = (Element) child;
406
407                                if (element.hasChildNodes()) {
408                                        start(element);
409                                        level++;
410
411                                        write(element);
412
413                                        level--;
414                                        end(element);
415                                } else {
416                                        //make childless elements appear as "<foo />" instead of "<foo></foo>"
417                                        childless(element);
418                                }
419
420                                continue;
421                        }
422
423                        if (child instanceof Text) {
424                                Text text = (Text) child;
425                                text(text.getTextContent());
426                                continue;
427                        }
428                }
429        }
430
431        private void write(ICalParameters parameters) throws SAXException {
432                if (parameters.isEmpty()) {
433                        return;
434                }
435
436                start(PARAMETERS);
437                level++;
438
439                for (Map.Entry<String, List<String>> parameter : parameters) {
440                        String parameterName = parameter.getKey().toLowerCase();
441                        start(parameterName);
442                        level++;
443
444                        for (String parameterValue : parameter.getValue()) {
445                                ICalDataType dataType = parameterDataTypes.get(parameterName);
446                                String dataTypeElementName = (dataType == null) ? "unknown" : dataType.getName().toLowerCase();
447
448                                start(dataTypeElementName);
449                                text(parameterValue);
450                                end(dataTypeElementName);
451                        }
452
453                        level--;
454                        end(parameterName);
455                }
456
457                level--;
458                end(PARAMETERS);
459        }
460
461        private void indent() throws SAXException {
462                if (indent == null) {
463                        return;
464                }
465
466                StringBuilder sb = new StringBuilder(NEWLINE);
467                for (int i = 0; i < level; i++) {
468                        sb.append(indent);
469                }
470
471                String str = sb.toString();
472                handler.ignorableWhitespace(str.toCharArray(), 0, str.length());
473        }
474
475        private void childless(Element element) throws SAXException {
476                Attributes attributes = getElementAttributes(element);
477                indent();
478                handler.startElement(element.getNamespaceURI(), "", element.getLocalName(), attributes);
479                handler.endElement(element.getNamespaceURI(), "", element.getLocalName());
480        }
481
482        private void start(Element element) throws SAXException {
483                Attributes attributes = getElementAttributes(element);
484                start(element.getNamespaceURI(), element.getLocalName(), attributes);
485        }
486
487        private void start(String element) throws SAXException {
488                start(element, null);
489        }
490
491        private void start(QName qname) throws SAXException {
492                start(qname, null);
493        }
494
495        private void start(QName qname, Attributes attributes) throws SAXException {
496                start(qname.getNamespaceURI(), qname.getLocalPart(), attributes);
497        }
498
499        private void start(String element, Attributes attributes) throws SAXException {
500                start(XCAL_NS, element, attributes);
501        }
502
503        private void start(String namespace, String element, Attributes attributes) throws SAXException {
504                indent();
505                handler.startElement(namespace, "", element, attributes);
506        }
507
508        private void end(Element element) throws SAXException {
509                end(element.getNamespaceURI(), element.getLocalName());
510        }
511
512        private void end(String element) throws SAXException {
513                end(XCAL_NS, element);
514        }
515
516        private void end(QName qname) throws SAXException {
517                end(qname.getNamespaceURI(), qname.getLocalPart());
518        }
519
520        private void end(String namespace, String element) throws SAXException {
521                if (!textNodeJustPrinted) {
522                        indent();
523                }
524
525                handler.endElement(namespace, "", element);
526                textNodeJustPrinted = false;
527        }
528
529        private void text(String text) throws SAXException {
530                handler.characters(text.toCharArray(), 0, text.length());
531                textNodeJustPrinted = true;
532        }
533
534        private Attributes getElementAttributes(Element element) {
535                AttributesImpl attributes = new AttributesImpl();
536                NamedNodeMap attributeNodes = element.getAttributes();
537                for (int i = 0; i < attributeNodes.getLength(); i++) {
538                        Node node = attributeNodes.item(i);
539                        attributes.addAttribute(node.getNamespaceURI(), "", node.getLocalName(), "", node.getNodeValue());
540                }
541                return attributes;
542        }
543
544        /**
545         * Terminates the XML document and closes the output stream.
546         */
547        public void close() throws IOException {
548                try {
549                        if (!started) {
550                                handler.startDocument();
551
552                                if (!icalendarElementExists) {
553                                        //don't output a <icalendar> element if the parent is a <icalendar> element
554                                        start(ICALENDAR);
555                                        level++;
556                                }
557                        }
558
559                        if (!icalendarElementExists) {
560                                level--;
561                                end(ICALENDAR);
562                        }
563                        handler.endDocument();
564                } catch (SAXException e) {
565                        throw new IOException(e);
566                }
567
568                if (writer != null) {
569                        writer.close();
570                }
571        }
572}