001package ezvcard.io.text;
002
003import static com.github.mangstadt.vinnie.Utils.escapeNewlines;
004
005import java.io.File;
006import java.io.FileWriter;
007import java.io.Flushable;
008import java.io.IOException;
009import java.io.OutputStream;
010import java.io.OutputStreamWriter;
011import java.io.StringWriter;
012import java.io.Writer;
013import java.util.ArrayList;
014import java.util.List;
015
016import com.github.mangstadt.vinnie.VObjectParameters;
017import com.github.mangstadt.vinnie.io.VObjectPropertyValues;
018import com.github.mangstadt.vinnie.io.VObjectWriter;
019
020import ezvcard.VCard;
021import ezvcard.VCardDataType;
022import ezvcard.VCardVersion;
023import ezvcard.io.EmbeddedVCardException;
024import ezvcard.io.SkipMeException;
025import ezvcard.io.StreamWriter;
026import ezvcard.io.scribe.VCardPropertyScribe;
027import ezvcard.parameter.VCardParameters;
028import ezvcard.property.Address;
029import ezvcard.property.BinaryProperty;
030import ezvcard.property.StructuredName;
031import ezvcard.property.VCardProperty;
032import ezvcard.util.IOUtils;
033import ezvcard.util.Utf8Writer;
034
035/*
036 Copyright (c) 2012-2018, Michael Angstadt
037 All rights reserved.
038
039 Redistribution and use in source and binary forms, with or without
040 modification, are permitted provided that the following conditions are met: 
041
042 1. Redistributions of source code must retain the above copyright notice, this
043 list of conditions and the following disclaimer. 
044 2. Redistributions in binary form must reproduce the above copyright notice,
045 this list of conditions and the following disclaimer in the documentation
046 and/or other materials provided with the distribution. 
047
048 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
049 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
050 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
051 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
052 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
053 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
054 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
055 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
056 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
057 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
058
059 The views and conclusions contained in the software and documentation are those
060 of the authors and should not be interpreted as representing official policies, 
061 either expressed or implied, of the FreeBSD Project.
062 */
063
064/**
065 * <p>
066 * Writes {@link VCard} objects to a plain-text vCard data stream.
067 * </p>
068 * <p>
069 * <b>Example:</b>
070 * </p>
071 * 
072 * <pre class="brush:java">
073 * VCard vcard1 = ...
074 * VCard vcard2 = ...
075 * File file = new File("vcard.vcf");
076 * VCardWriter writer = null;
077 * try {
078 *   writer = new VCardWriter(file, VCardVersion.V3_0);
079 *   writer.write(vcard1);
080 *   writer.write(vcard2);
081 * } finally {
082 *   if (writer != null) writer.close();
083 * }
084 * </pre>
085 * 
086 * <p>
087 * <b>Changing the line folding settings:</b>
088 * </p>
089 * 
090 * <pre class="brush:java">
091 * VCardWriter writer = new VCardWriter(...);
092 * 
093 * //disable line folding
094 * writer.getVObjectWriter().getFoldedLineWriter().setLineLength(null);
095 * 
096 * //change line length
097 * writer.getVObjectWriter().getFoldedLineWriter().setLineLength(50);
098 * 
099 * //change folded line indent string
100 * writer.getVObjectWriter().getFoldedLineWriter().setIndent("\t");
101 * 
102 * </pre>
103 * @author Michael Angstadt
104 * @see <a href="http://www.imc.org/pdi/vcard-21.rtf">vCard 2.1</a>
105 * @see <a href="http://tools.ietf.org/html/rfc2426">RFC 2426 (3.0)</a>
106 * @see <a href="http://tools.ietf.org/html/rfc6350">RFC 6350 (4.0)</a>
107 */
108public class VCardWriter extends StreamWriter implements Flushable {
109        private final VObjectWriter writer;
110        private final List<Boolean> prodIdStack = new ArrayList<Boolean>();
111        private VCardVersion targetVersion;
112        private TargetApplication targetApplication;
113        private Boolean includeTrailingSemicolons;
114
115        /**
116         * @param out the output stream to write to
117         * @param targetVersion the version that the vCards should conform to (if
118         * set to "4.0", vCards will be written in UTF-8 encoding)
119         */
120        public VCardWriter(OutputStream out, VCardVersion targetVersion) {
121                this((targetVersion == VCardVersion.V4_0) ? new Utf8Writer(out) : new OutputStreamWriter(out), targetVersion);
122        }
123
124        /**
125         * @param file the file to write to
126         * @param targetVersion the version that the vCards should conform to (if
127         * set to "4.0", vCards will be written in UTF-8 encoding)
128         * @throws IOException if there's a problem opening the file
129         */
130        public VCardWriter(File file, VCardVersion targetVersion) throws IOException {
131                this(file, false, targetVersion);
132        }
133
134        /**
135         * @param file the file to write to
136         * @param append true to append to the end of the file, false to overwrite
137         * it
138         * @param targetVersion the version that the vCards should conform to (if
139         * set to "4.0", vCards will be written in UTF-8 encoding)
140         * @throws IOException if there's a problem opening the file
141         */
142        public VCardWriter(File file, boolean append, VCardVersion targetVersion) throws IOException {
143                this((targetVersion == VCardVersion.V4_0) ? new Utf8Writer(file, append) : new FileWriter(file, append), targetVersion);
144        }
145
146        /**
147         * @param writer the writer to write to
148         * @param targetVersion the version that the vCards should conform to
149         */
150        public VCardWriter(Writer writer, VCardVersion targetVersion) {
151                this.writer = new VObjectWriter(writer, targetVersion.getSyntaxStyle());
152                this.targetVersion = targetVersion;
153        }
154
155        /**
156         * Gets the writer that this object uses to write data to the output stream.
157         * @return the writer
158         */
159        public VObjectWriter getVObjectWriter() {
160                return writer;
161        }
162
163        /**
164         * Gets the version that the vCards should adhere to.
165         * @return the vCard version
166         */
167        @Override
168        public VCardVersion getTargetVersion() {
169                return targetVersion;
170        }
171
172        /**
173         * Sets the version that the vCards should adhere to.
174         * @param targetVersion the vCard version
175         */
176        public void setTargetVersion(VCardVersion targetVersion) {
177                writer.setSyntaxStyle(targetVersion.getSyntaxStyle());
178                this.targetVersion = targetVersion;
179        }
180
181        /**
182         * <p>
183         * Gets the application that the vCards will be targeted for.
184         * </p>
185         * <p>
186         * Some vCard consumers do not completely adhere to the vCard specifications
187         * and require their vCards to be formatted in a specific way. See the
188         * {@link TargetApplication} class for a list of these applications.
189         * </p>
190         * @return the target application or null if the vCards do not be given any
191         * special processing (defaults to null)
192         */
193        public TargetApplication getTargetApplication() {
194                return targetApplication;
195        }
196
197        /**
198         * <p>
199         * Sets the application that the vCards will be targeted for.
200         * </p>
201         * <p>
202         * Some vCard consumers do not completely adhere to the vCard specifications
203         * and require their vCards to be formatted in a specific way. See the
204         * {@link TargetApplication} class for a list of these applications.
205         * </p>
206         * @param targetApplication the target application or null if the vCards do
207         * not require any special processing (defaults to null)
208         */
209        public void setTargetApplication(TargetApplication targetApplication) {
210                this.targetApplication = targetApplication;
211        }
212
213        /**
214         * <p>
215         * Gets whether this writer will include trailing semicolon delimiters for
216         * structured property values whose list of values end with null or empty
217         * values. Examples of properties that use structured values are
218         * {@link StructuredName} and {@link Address}.
219         * </p>
220         * <p>
221         * This setting exists for compatibility reasons and should not make a
222         * difference to consumers that correctly implement the vCard grammar.
223         * </p>
224         * @return true to include the trailing semicolons, false not to, null to
225         * use the default behavior (defaults to false for vCard versions 2.1 and
226         * 3.0 and true for vCard version 4.0)
227         * @see <a href="https://github.com/mangstadt/ez-vcard/issues/57">Issue
228         * 57</a>
229         */
230        public Boolean isIncludeTrailingSemicolons() {
231                return includeTrailingSemicolons;
232        }
233
234        /**
235         * <p>
236         * Sets whether to include trailing semicolon delimiters for structured
237         * property values whose list of values end with null or empty values.
238         * Examples of properties that use structured values are
239         * {@link StructuredName} and {@link Address}.
240         * </p>
241         * <p>
242         * This setting exists for compatibility reasons and should not make a
243         * difference to consumers that correctly implement the vCard grammar.
244         * </p>
245         * @param include true to include the trailing semicolons, false not to,
246         * null to use the default behavior (defaults to false for vCard versions
247         * 2.1 and 3.0 and true for vCard version 4.0)
248         * @see <a href="https://github.com/mangstadt/ez-vcard/issues/57">Issue
249         * 57</a>
250         */
251        public void setIncludeTrailingSemicolons(Boolean include) {
252                includeTrailingSemicolons = include;
253        }
254
255        /**
256         * <p>
257         * Gets whether the writer will apply circumflex accent encoding on
258         * parameter values (disabled by default). This escaping mechanism allows
259         * for newlines and double quotes to be included in parameter values. It is
260         * only supported by vCard versions 3.0 and 4.0.
261         * </p>
262         * 
263         * <p>
264         * Note that this encoding mechanism is defined separately from the vCard
265         * specification and may not be supported by the consumer of the vCard.
266         * </p>
267         * @return true if circumflex accent encoding is enabled, false if not
268         * @see VObjectWriter#isCaretEncodingEnabled()
269         */
270        public boolean isCaretEncodingEnabled() {
271                return writer.isCaretEncodingEnabled();
272        }
273
274        /**
275         * <p>
276         * Sets whether the writer will apply circumflex accent encoding on
277         * parameter values (disabled by default). This escaping mechanism allows
278         * for newlines and double quotes to be included in parameter values. It is
279         * only supported by vCard versions 3.0 and 4.0.
280         * </p>
281         * 
282         * <p>
283         * Note that this encoding mechanism is defined separately from the vCard
284         * specification and may not be supported by the consumer of the vCard.
285         * </p>
286         * @param enable true to use circumflex accent encoding, false not to
287         * @see VObjectWriter#setCaretEncodingEnabled(boolean)
288         */
289        public void setCaretEncodingEnabled(boolean enable) {
290                writer.setCaretEncodingEnabled(enable);
291        }
292
293        @Override
294        @SuppressWarnings({ "rawtypes", "unchecked" })
295        protected void _write(VCard vcard, List<VCardProperty> propertiesToAdd) throws IOException {
296                VCardVersion targetVersion = getTargetVersion();
297                TargetApplication targetApplication = getTargetApplication();
298
299                Boolean includeTrailingSemicolons = this.includeTrailingSemicolons;
300                if (includeTrailingSemicolons == null) {
301                        includeTrailingSemicolons = (targetVersion == VCardVersion.V4_0);
302                }
303
304                WriteContext context = new WriteContext(targetVersion, targetApplication, includeTrailingSemicolons);
305
306                writer.writeBeginComponent("VCARD");
307                writer.writeVersion(targetVersion.getVersion());
308
309                for (VCardProperty property : propertiesToAdd) {
310                        VCardPropertyScribe scribe = index.getPropertyScribe(property);
311
312                        String value = null;
313                        VCard nestedVCard = null;
314                        try {
315                                value = scribe.writeText(property, context);
316                        } catch (SkipMeException e) {
317                                continue;
318                        } catch (EmbeddedVCardException e) {
319                                nestedVCard = e.getVCard();
320                        }
321
322                        VCardParameters parameters = scribe.prepareParameters(property, targetVersion, vcard);
323
324                        if (nestedVCard != null) {
325                                writeNestedVCard(nestedVCard, property, scribe, parameters, value);
326                                continue;
327                        }
328
329                        handleValueParameter(property, scribe, parameters);
330                        handleLabelParameter(property, parameters);
331
332                        writer.writeProperty(property.getGroup(), scribe.getPropertyName(), new VObjectParameters(parameters.getMap()), value);
333
334                        fixBinaryPropertyForOutlook(property);
335                }
336
337                writer.writeEndComponent("VCARD");
338        }
339
340        @SuppressWarnings("rawtypes")
341        private void writeNestedVCard(VCard nestedVCard, VCardProperty property, VCardPropertyScribe scribe, VCardParameters parameters, String value) throws IOException {
342                if (targetVersion == VCardVersion.V2_1) {
343                        //write a nested vCard (2.1 style)
344                        writer.writeProperty(property.getGroup(), scribe.getPropertyName(), new VObjectParameters(parameters.getMap()), value);
345                        prodIdStack.add(addProdId);
346                        addProdId = false;
347                        write(nestedVCard);
348                        addProdId = prodIdStack.remove(prodIdStack.size() - 1);
349                } else {
350                        //write an embedded vCard (3.0 style)
351                        StringWriter sw = new StringWriter();
352                        VCardWriter agentWriter = new VCardWriter(sw, targetVersion);
353                        agentWriter.getVObjectWriter().getFoldedLineWriter().setLineLength(null);
354                        agentWriter.setAddProdId(false);
355                        agentWriter.setCaretEncodingEnabled(isCaretEncodingEnabled());
356                        agentWriter.setIncludeTrailingSemicolons(this.includeTrailingSemicolons);
357                        agentWriter.setScribeIndex(index);
358                        agentWriter.setTargetApplication(targetApplication);
359                        agentWriter.setVersionStrict(versionStrict);
360                        try {
361                                agentWriter.write(nestedVCard);
362                        } catch (IOException e) {
363                                //should never be thrown because we're writing to a string
364                        } finally {
365                                IOUtils.closeQuietly(agentWriter);
366                        }
367
368                        String vcardStr = sw.toString();
369                        vcardStr = VObjectPropertyValues.escape(vcardStr);
370                        writer.writeProperty(property.getGroup(), scribe.getPropertyName(), new VObjectParameters(parameters.getMap()), vcardStr);
371                }
372        }
373
374        /**
375         * <p>
376         * Sets the property's VALUE parameter. This method only adds a VALUE
377         * parameter if all the following conditions are met:
378         * </p>
379         * <ol>
380         * <li>The data type is NOT "unknown"</li>
381         * <li>The data type is different from the property's default data type</li>
382         * <li>The data type does not fall under the "date/time special case" (see
383         * {@link #isDateTimeValueParameterSpecialCase()})</li>
384         * </ol>
385         * @param property the property
386         * @param scribe the property scribe
387         * @param parameters the property parameters
388         */
389        @SuppressWarnings({ "rawtypes", "unchecked" })
390        private void handleValueParameter(VCardProperty property, VCardPropertyScribe scribe, VCardParameters parameters) {
391                VCardDataType dataType = scribe.dataType(property, targetVersion);
392                if (dataType == null) {
393                        return;
394                }
395
396                VCardDataType defaultDataType = scribe.defaultDataType(targetVersion);
397                if (dataType == defaultDataType) {
398                        return;
399                }
400
401                if (isDateTimeValueParameterSpecialCase(defaultDataType, dataType)) {
402                        return;
403                }
404
405                parameters.setValue(dataType);
406        }
407
408        /**
409         * <p>
410         * Escapes newline sequences in the LABEL parameter of {@link Address}
411         * properties. Newlines cannot normally be escaped in parameter values.
412         * </p>
413         * <p>
414         * Only version 4.0 allows this (and only version 4.0 defines a LABEL
415         * parameter), but this method does this for all versions for compatibility.
416         * </p>
417         * @param property the property
418         * @param parameters the property parameters
419         */
420        private void handleLabelParameter(VCardProperty property, VCardParameters parameters) {
421                if (!(property instanceof Address)) {
422                        return;
423                }
424
425                String label = parameters.getLabel();
426                if (label == null) {
427                        return;
428                }
429
430                label = escapeNewlines(label);
431                parameters.setLabel(label);
432        }
433
434        /**
435         * @see TargetApplication#OUTLOOK
436         */
437        private void fixBinaryPropertyForOutlook(VCardProperty property) throws IOException {
438                if (targetApplication != TargetApplication.OUTLOOK) {
439                        return;
440                }
441
442                if (getTargetVersion() == VCardVersion.V4_0) {
443                        //only do this for 2.1 and 3.0 vCards
444                        return;
445                }
446
447                if (!(property instanceof BinaryProperty)) {
448                        //property does not have binary data
449                        return;
450                }
451
452                BinaryProperty<?> binaryProperty = (BinaryProperty<?>) property;
453                if (binaryProperty.getData() == null) {
454                        //property value is not base64-encoded
455                        return;
456                }
457
458                writer.getFoldedLineWriter().writeln();
459        }
460
461        /**
462         * Determines if the given default data type is "date-and-or-time" and the
463         * given data type is time-based. Properties that meet this criteria should
464         * NOT be given a VALUE parameter.
465         * @param defaultDataType the property's default data type
466         * @param dataType the current property instance's data type
467         * @return true if the default data type is "date-and-or-time" and the data
468         * type is time-based, false otherwise
469         */
470        private boolean isDateTimeValueParameterSpecialCase(VCardDataType defaultDataType, VCardDataType dataType) {
471                //@formatter:off
472                return
473                defaultDataType == VCardDataType.DATE_AND_OR_TIME &&
474                (
475                        dataType == VCardDataType.DATE ||
476                        dataType == VCardDataType.DATE_TIME ||
477                        dataType == VCardDataType.TIME
478                );
479                //@formatter:on
480        }
481
482        /**
483         * Flushes the output stream.
484         * @throws IOException if there's a problem flushing the output stream
485         */
486        public void flush() throws IOException {
487                writer.flush();
488        }
489
490        /**
491         * Closes the output stream.
492         * @throws IOException if there's a problem closing the output stream
493         */
494        public void close() throws IOException {
495                writer.close();
496        }
497}