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 }