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 }