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}