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