001    package ezvcard.io.text;
002    
003    import static ezvcard.util.IOUtils.utf8Writer;
004    
005    import java.io.Closeable;
006    import java.io.File;
007    import java.io.FileWriter;
008    import java.io.Flushable;
009    import java.io.IOException;
010    import java.io.OutputStream;
011    import java.io.OutputStreamWriter;
012    import java.io.StringWriter;
013    import java.io.Writer;
014    import java.util.ArrayList;
015    import java.util.List;
016    
017    import ezvcard.Ezvcard;
018    import ezvcard.VCard;
019    import ezvcard.VCardDataType;
020    import ezvcard.VCardVersion;
021    import ezvcard.io.EmbeddedVCardException;
022    import ezvcard.io.SkipMeException;
023    import ezvcard.io.scribe.ScribeIndex;
024    import ezvcard.io.scribe.VCardPropertyScribe;
025    import ezvcard.parameter.AddressType;
026    import ezvcard.parameter.VCardParameters;
027    import ezvcard.property.Address;
028    import ezvcard.property.Label;
029    import ezvcard.property.ProductId;
030    import ezvcard.property.RawProperty;
031    import ezvcard.property.VCardProperty;
032    import ezvcard.util.IOUtils;
033    
034    /*
035     Copyright (c) 2013, Michael Angstadt
036     All rights reserved.
037    
038     Redistribution and use in source and binary forms, with or without
039     modification, are permitted provided that the following conditions are met: 
040    
041     1. Redistributions of source code must retain the above copyright notice, this
042     list of conditions and the following disclaimer. 
043     2. Redistributions in binary form must reproduce the above copyright notice,
044     this list of conditions and the following disclaimer in the documentation
045     and/or other materials provided with the distribution. 
046    
047     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
048     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
049     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
050     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
051     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
052     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
053     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
054     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
055     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
056     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
057    
058     The views and conclusions contained in the software and documentation are those
059     of the authors and should not be interpreted as representing official policies, 
060     either expressed or implied, of the FreeBSD Project.
061     */
062    
063    /**
064     * <p>
065     * Writes {@link VCard} objects to a plain-text vCard data stream.
066     * </p>
067     * <p>
068     * <b>Example:</b>
069     * 
070     * <pre class="brush:java">
071     * VCard vcard1 = ...
072     * VCard vcard2 = ...
073     * 
074     * File file = new File("vcard.vcf");
075     * VCardWriter vcardWriter = new VCardWriter(file);
076     * vcardWriter.write(vcard1);
077     * vcardWriter.write(vcard2);
078     * vcardWriter.close();
079     * </pre>
080     * 
081     * </p>
082     * @author Michael Angstadt
083     */
084    public class VCardWriter implements Closeable, Flushable {
085            private ScribeIndex index = new ScribeIndex();
086            private boolean addProdId = true;
087            private boolean versionStrict = true;
088            private final VCardRawWriter writer;
089    
090            /**
091             * Creates a writer that writes vCards to an output stream (writes v3.0
092             * vCards and uses the standard folding scheme and newline sequence).
093             * @param out the output stream to write the vCard to
094             */
095            public VCardWriter(OutputStream out) {
096                    this(new OutputStreamWriter(out));
097            }
098    
099            /**
100             * Creates a writer that writes vCards to an output stream (uses the
101             * standard folding scheme and newline sequence).
102             * @param out the output stream to write the vCard to
103             * @param targetVersion the version that the vCards should conform to (if
104             * set to "4.0", vCards will be written in UTF-8 encoding)
105             */
106            public VCardWriter(OutputStream out, VCardVersion targetVersion) {
107                    this((targetVersion == VCardVersion.V4_0) ? utf8Writer(out) : new OutputStreamWriter(out), targetVersion);
108            }
109    
110            /**
111             * Creates a writer that writes vCards to an output stream.
112             * @param out the output stream to write the vCard to
113             * @param targetVersion the version that the vCards should conform to (if
114             * set to "4.0", vCards will be written in UTF-8 encoding)
115             * @param foldingScheme the folding scheme to use or null not to fold at all
116             * @param newline the newline sequence to use
117             */
118            public VCardWriter(OutputStream out, VCardVersion targetVersion, FoldingScheme foldingScheme, String newline) {
119                    this((targetVersion == VCardVersion.V4_0) ? utf8Writer(out) : new OutputStreamWriter(out), targetVersion, foldingScheme, newline);
120            }
121    
122            /**
123             * Creates a writer that writes vCards to a file (writes v3.0 vCards and
124             * uses the standard folding scheme and newline sequence).
125             * @param file the file to write the vCard to
126             * @throws IOException if there's a problem opening the file
127             */
128            public VCardWriter(File file) throws IOException {
129                    this(new FileWriter(file, false));
130            }
131    
132            /**
133             * Creates a writer that writes vCards to a file (writes v3.0 vCards and
134             * uses the standard folding scheme and newline sequence).
135             * @param file the file to write the vCard to
136             * @param append true to append to the end of the file, false to overwrite
137             * it
138             * @throws IOException if there's a problem opening the file
139             */
140            public VCardWriter(File file, boolean append) throws IOException {
141                    this(new FileWriter(file, append));
142            }
143    
144            /**
145             * Creates a writer that writes vCards to a file (uses the standard folding
146             * scheme and newline sequence).
147             * @param file the file to write the vCard to
148             * @param append true to append to the end of the file, false to overwrite
149             * it
150             * @param targetVersion the version that the vCards should conform to (if
151             * set to "4.0", vCards will be written in UTF-8 encoding)
152             * @throws IOException if there's a problem opening the file
153             */
154            public VCardWriter(File file, boolean append, VCardVersion targetVersion) throws IOException {
155                    this((targetVersion == VCardVersion.V4_0) ? utf8Writer(file, append) : new FileWriter(file, append), targetVersion);
156            }
157    
158            /**
159             * Creates a writer that writes vCards to a file.
160             * @param file the file to write the vCard to
161             * @param append true to append to the end of the file, false to overwrite
162             * it
163             * @param targetVersion the version that the vCards should conform to (if
164             * set to "4.0", vCards will be written in UTF-8 encoding)
165             * @param foldingScheme the folding scheme to use or null not to fold at all
166             * @param newline the newline sequence to use
167             * @throws IOException if there's a problem opening the file
168             */
169            public VCardWriter(File file, boolean append, VCardVersion targetVersion, FoldingScheme foldingScheme, String newline) throws IOException {
170                    this((targetVersion == VCardVersion.V4_0) ? utf8Writer(file, append) : new FileWriter(file, append), targetVersion, foldingScheme, newline);
171            }
172    
173            /**
174             * Creates a writer that writes vCards to a writer (writes v3.0 vCards and
175             * uses the standard folding scheme and newline sequence).
176             * @param writer the writer to write the vCard to
177             */
178            public VCardWriter(Writer writer) {
179                    this(writer, VCardVersion.V3_0);
180            }
181    
182            /**
183             * Creates a writer that writes vCards to a writer (uses the standard
184             * folding scheme and newline sequence).
185             * @param writer the writer to write the vCard to
186             * @param targetVersion the version that the vCards should conform to
187             */
188            public VCardWriter(Writer writer, VCardVersion targetVersion) {
189                    this(writer, targetVersion, FoldingScheme.MIME_DIR, "\r\n");
190            }
191    
192            /**
193             * Creates a writer that writes vCards to a writer.
194             * @param writer the writer to write the vCard to
195             * @param targetVersion the version that the vCards should conform to
196             * @param foldingScheme the folding scheme to use or null not to fold at all
197             * @param newline the newline sequence to use
198             */
199            public VCardWriter(Writer writer, VCardVersion targetVersion, FoldingScheme foldingScheme, String newline) {
200                    this.writer = new VCardRawWriter(writer, targetVersion, foldingScheme, newline);
201            }
202    
203            /**
204             * Gets the version that the vCards should adhere to.
205             * @return the vCard version
206             */
207            public VCardVersion getTargetVersion() {
208                    return writer.getVersion();
209            }
210    
211            /**
212             * Sets the version that the vCards should adhere to.
213             * @param targetVersion the vCard version
214             */
215            public void setTargetVersion(VCardVersion targetVersion) {
216                    writer.setVersion(targetVersion);
217            }
218    
219            /**
220             * Gets whether or not a "PRODID" property will be added to each vCard,
221             * saying that the vCard was generated by this library. For 2.1 vCards, the
222             * extended property "X-PRODID" will be added, since "PRODID" is not
223             * supported by that version.
224             * @return true if the property will be added, false if not (defaults to
225             * true)
226             */
227            public boolean isAddProdId() {
228                    return addProdId;
229            }
230    
231            /**
232             * Sets whether or not to add a "PRODID" property to each vCard, saying that
233             * the vCard was generated by this library. For 2.1 vCards, the extended
234             * property "X-PRODID" will be added, since "PRODID" is not supported by
235             * that version.
236             * @param addProdId true to add this property, false not to (defaults to
237             * true)
238             */
239            public void setAddProdId(boolean addProdId) {
240                    this.addProdId = addProdId;
241            }
242    
243            /**
244             * Gets whether properties that do not support the target version will be
245             * excluded from the written vCard.
246             * @return true to exclude properties that do not support the target
247             * version, false to include them anyway (defaults to true)
248             */
249            public boolean isVersionStrict() {
250                    return versionStrict;
251            }
252    
253            /**
254             * Sets whether properties that do not support the target version will be
255             * excluded from the written vCard.
256             * @param versionStrict true to exclude properties that do not support the
257             * target version, false to include them anyway (defaults to true)
258             */
259            public void setVersionStrict(boolean versionStrict) {
260                    this.versionStrict = versionStrict;
261            }
262    
263            /**
264             * <p>
265             * Gets whether the writer will apply circumflex accent encoding on
266             * parameter values (disabled by default, only applies to 3.0 and 4.0
267             * vCards). This escaping mechanism allows for newlines and double quotes to
268             * be included in parameter values.
269             * </p>
270             * 
271             * <p>
272             * When disabled, the writer will replace newlines with spaces and double
273             * quotes with single quotes.
274             * </p>
275             * @return true if circumflex accent encoding is enabled, false if not
276             * @see VCardRawWriter#isCaretEncodingEnabled()
277             */
278            public boolean isCaretEncodingEnabled() {
279                    return writer.isCaretEncodingEnabled();
280            }
281    
282            /**
283             * <p>
284             * Sets whether the writer will apply circumflex accent encoding on
285             * parameter values (disabled by default, only applies to 3.0 and 4.0
286             * vCards). This escaping mechanism allows for newlines and double quotes to
287             * be included in parameter values.
288             * </p>
289             * 
290             * <p>
291             * When disabled, the writer will replace newlines with spaces and double
292             * quotes with single quotes.
293             * </p>
294             * @param enable true to use circumflex accent encoding, false not to
295             * @see VCardRawWriter#setCaretEncodingEnabled(boolean)
296             */
297            public void setCaretEncodingEnabled(boolean enable) {
298                    writer.setCaretEncodingEnabled(enable);
299            }
300    
301            /**
302             * Gets the newline sequence that is used to separate lines.
303             * @return the newline sequence
304             */
305            public String getNewline() {
306                    return writer.getNewline();
307            }
308    
309            /**
310             * Gets the rules for how each line is folded.
311             * @return the folding scheme or null if the lines are not folded
312             */
313            public FoldingScheme getFoldingScheme() {
314                    return writer.getFoldingScheme();
315            }
316    
317            /**
318             * <p>
319             * Registers a property scribe. This is the same as calling:
320             * </p>
321             * <p>
322             * {@code getScribeIndex().register(scribe)}
323             * </p>
324             * @param scribe the scribe to register
325             */
326            public void registerScribe(VCardPropertyScribe<? extends VCardProperty> scribe) {
327                    index.register(scribe);
328            }
329    
330            /**
331             * Gets the scribe index.
332             * @return the scribe index
333             */
334            public ScribeIndex getScribeIndex() {
335                    return index;
336            }
337    
338            /**
339             * Sets the scribe index.
340             * @param index the scribe index
341             */
342            public void setScribeIndex(ScribeIndex index) {
343                    this.index = index;
344            }
345    
346            /**
347             * Writes a vCard to the stream.
348             * @param vcard the vCard to write
349             * @throws IOException if there's a problem writing to the output stream
350             * @throws IllegalArgumentException if a scribe hasn't been registered for a
351             * custom property class (see: {@link #registerScribe})
352             */
353            public void write(VCard vcard) throws IOException {
354                    write(vcard, addProdId);
355            }
356    
357            @SuppressWarnings({ "rawtypes", "unchecked" })
358            private void write(VCard vcard, boolean addProdId) throws IOException {
359                    VCardVersion targetVersion = writer.getVersion();
360    
361                    List<VCardProperty> typesToAdd = new ArrayList<VCardProperty>();
362                    for (VCardProperty type : vcard) {
363                            if (addProdId && type instanceof ProductId) {
364                                    //do not add the PRODID in the vCard if "addProdId" is true
365                                    continue;
366                            }
367    
368                            if (versionStrict && !type.getSupportedVersions().contains(targetVersion)) {
369                                    //do not add the property to the vCard if it is not supported by the target version
370                                    continue;
371                            }
372    
373                            //check for scribes before writing anything to the stream
374                            if (index.getPropertyScribe(type) == null) {
375                                    throw new IllegalArgumentException("No scribe found for property class \"" + type.getClass().getName() + "\".");
376                            }
377    
378                            typesToAdd.add(type);
379    
380                            //add LABEL types for each ADR type if the target version is 2.1 or 3.0
381                            if (type instanceof Address && (targetVersion == VCardVersion.V2_1 || targetVersion == VCardVersion.V3_0)) {
382                                    Address adr = (Address) type;
383                                    String labelStr = adr.getLabel();
384                                    if (labelStr != null) {
385                                            Label label = new Label(labelStr);
386                                            for (AddressType t : adr.getTypes()) {
387                                                    label.addType(t);
388                                            }
389                                            typesToAdd.add(label);
390                                    }
391                            }
392                    }
393    
394                    //add an extended type saying it was generated by this library
395                    if (addProdId) {
396                            VCardProperty property;
397                            if (targetVersion == VCardVersion.V2_1) {
398                                    property = new RawProperty("X-PRODID", "ezvcard " + Ezvcard.VERSION);
399                            } else {
400                                    property = new ProductId("ezvcard " + Ezvcard.VERSION);
401                            }
402                            typesToAdd.add(property);
403                    }
404    
405                    writer.writeBeginComponent("VCARD");
406                    writer.writeVersion();
407    
408                    for (VCardProperty type : typesToAdd) {
409                            VCardPropertyScribe scribe = index.getPropertyScribe(type);
410    
411                            //marshal the value
412                            String value = null;
413                            VCard nestedVCard = null;
414                            try {
415                                    value = scribe.writeText(type, targetVersion);
416                            } catch (SkipMeException e) {
417                                    continue;
418                            } catch (EmbeddedVCardException e) {
419                                    nestedVCard = e.getVCard();
420                            }
421    
422                            //marshal the sub types
423                            VCardParameters parameters = scribe.prepareParameters(type, targetVersion, vcard);
424    
425                            if (nestedVCard == null) {
426                                    //set the data type
427                                    //only add a VALUE parameter if the data type is (1) not "unknown" and (2) different from the property's default data type
428                                    VCardDataType dataType = scribe.dataType(type, targetVersion);
429                                    if (dataType != null) {
430                                            VCardDataType defaultDataType = scribe.defaultDataType(targetVersion);
431                                            if (dataType != defaultDataType) {
432                                                    if (defaultDataType == VCardDataType.DATE_AND_OR_TIME && (dataType == VCardDataType.DATE || dataType == VCardDataType.DATE_TIME || dataType == VCardDataType.TIME)) {
433                                                            //do not write VALUE if the default data type is "date-and-or-time" and the property's data type is time-based
434                                                    } else {
435                                                            parameters.setValue(dataType);
436                                                    }
437                                            }
438                                    }
439    
440                                    writer.writeProperty(type.getGroup(), scribe.getPropertyName(), parameters, value);
441                            } else {
442                                    if (targetVersion == VCardVersion.V2_1) {
443                                            //write a nested vCard (2.1 style)
444                                            writer.writeProperty(type.getGroup(), scribe.getPropertyName(), parameters, value);
445                                            write(nestedVCard, false);
446                                    } else {
447                                            //write an embedded vCard (3.0 style)
448                                            StringWriter sw = new StringWriter();
449                                            VCardWriter agentWriter = new VCardWriter(sw, targetVersion, null, "\n");
450                                            agentWriter.setAddProdId(false);
451                                            agentWriter.setVersionStrict(versionStrict);
452                                            try {
453                                                    agentWriter.write(nestedVCard);
454                                            } catch (IOException e) {
455                                                    //writing to a string
456                                            } finally {
457                                                    IOUtils.closeQuietly(agentWriter);
458                                            }
459    
460                                            String vCardStr = sw.toString();
461                                            vCardStr = VCardPropertyScribe.escape(vCardStr);
462                                            writer.writeProperty(type.getGroup(), scribe.getPropertyName(), parameters, vCardStr);
463                                    }
464                            }
465                    }
466    
467                    writer.writeEndComponent("VCARD");
468            }
469    
470            /**
471             * Flushes the underlying {@link Writer} object.
472             * @throws IOException if there's a problem flushing the writer
473             */
474            public void flush() throws IOException {
475                    writer.flush();
476            }
477    
478            /**
479             * Closes the underlying {@link Writer} object.
480             * @throws IOException if there's a problem closing the writer
481             */
482            public void close() throws IOException {
483                    writer.close();
484            }
485    }