001/* 002 * MIT License 003 * 004 * Copyright (c) 2016 Michael Angstadt 005 * 006 * Permission is hereby granted, free of charge, to any person obtaining a copy 007 * of this software and associated documentation files (the "Software"), to deal 008 * in the Software without restriction, including without limitation the rights 009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 010 * copies of the Software, and to permit persons to whom the Software is 011 * furnished to do so, subject to the following conditions: 012 * 013 * The above copyright notice and this permission notice shall be included in 014 * all copies or substantial portions of the Software. 015 * 016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 022 * SOFTWARE. 023 */ 024 025package com.github.mangstadt.vinnie.io; 026 027import static com.github.mangstadt.vinnie.Utils.escapeNewlines; 028 029import java.io.Closeable; 030import java.io.Flushable; 031import java.io.IOException; 032import java.io.Writer; 033import java.nio.charset.Charset; 034import java.util.List; 035import java.util.Map; 036 037import com.github.mangstadt.vinnie.SyntaxStyle; 038import com.github.mangstadt.vinnie.VObjectParameters; 039import com.github.mangstadt.vinnie.VObjectProperty; 040import com.github.mangstadt.vinnie.validate.AllowedCharacters; 041import com.github.mangstadt.vinnie.validate.VObjectValidator; 042 043/** 044 * <p> 045 * Writes data to a vobject data stream. 046 * </p> 047 * <p> 048 * <b>Example:</b> 049 * </p> 050 * 051 * <pre class="brush:java"> 052 * Writer writer = ... 053 * VObjectWriter vobjectWriter = new VObjectWriter(writer, SyntaxStyle.NEW); 054 * vobjectWriter.writeBeginComponent("VCARD"); 055 * vobjectWriter.writeVersion("4.0"); 056 * vobjectWriter.writeProperty("FN", "John Doe"); 057 * vobjectWriter.writeEndComponent("VCARD"); 058 * vobjectWriter.close(); 059 * </pre> 060 * 061 * <p> 062 * <b>Invalid characters</b> 063 * </p> 064 * <p> 065 * If property data contains any invalid characters, the {@code writeProperty} 066 * method throws an {@link IllegalArgumentException} and the property is not 067 * written. A character is considered to be invalid if it cannot be encoded or 068 * escaped, and would break the vobject syntax if written. 069 * </p> 070 * <p> 071 * The rules regarding which characters are considered invalid is fairly 072 * complex. Here are some general guidelines: 073 * </p> 074 * <ul> 075 * <li>Try to limit group names, property names, and parameter names to 076 * alphanumerics and hyphens.</li> 077 * <li>Avoid the use of newlines, double quotes, and colons inside of parameter 078 * values. They can be used in some contexts, but not others.</li> 079 * </ul> 080 * 081 * <p> 082 * <b>Newlines in property values</b> 083 * </p> 084 * <p> 085 * All newline characters ("\r" or "\n") within property values are 086 * automatically escaped. 087 * </p> 088 * <p> 089 * In old-style syntax, the property value will be encoded in quoted-printable 090 * encoding. 091 * </p> 092 * 093 * <pre class="brush:java"> 094 * StringWriter sw = new StringWriter(); 095 * VObjectWriter vobjectWriter = new VObjectWriter(sw, SyntaxStyle.OLD); 096 * vobjectWriter.writeProperty("NOTE", "one\r\ntwo"); 097 * vobjectWriter.close(); 098 * 099 * assertEquals("NOTE;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:one=0D=0Atwo\r\n", sw.toString()); 100 * </pre> 101 * 102 * <p> 103 * In new-style syntax, the newline characters will be replaced with the "\n" 104 * escape sequence (Windows newline sequences are replaced with a single "\n" 105 * even though they consist of two characters). 106 * </p> 107 * 108 * <pre class="brush:java"> 109 * StringWriter sw = new StringWriter(); 110 * VObjectWriter vobjectWriter = new VObjectWriter(sw, SyntaxStyle.NEW); 111 * vobjectWriter.writeProperty("NOTE", "one\r\ntwo"); 112 * vobjectWriter.close(); 113 * 114 * assertEquals("NOTE:one\\ntwo\r\n", sw.toString()); 115 * </pre> 116 * 117 * <p> 118 * <b>Quoted-printable Encoding</b> 119 * </p> 120 * <p> 121 * If a property has a parameter named ENCODING that has a value of 122 * QUOTED-PRINTABLE (case-insensitive), then the property's value will 123 * automatically be written in quoted-printable encoding. 124 * </p> 125 * 126 * <pre class="brush:java"> 127 * StringWriter sw = new StringWriter(); 128 * VObjectWriter vobjectWriter = new VObjectWriter(sw, ...); 129 * 130 * VObjectProperty note = new VObjectProperty("NOTE", "¡Hola, mundo!"); 131 * note.getParameters().put("ENCODING", "QUOTED-PRINTABLE"); 132 * vobjectWriter.writeProperty(note); 133 * vobjectWriter.close(); 134 * 135 * assertEquals("NOTE;ENCODING=QUOTED-PRINTABLE;CHARSET=UTF-8:=C2=A1Hola, mundo!\r\n", sw.toString()); 136 * </pre> 137 * <p> 138 * A nameless parameter may also be used for backwards compatibility with 139 * old-style syntax. 140 * </p> 141 * 142 * <pre class="brush:java"> 143 * VObjectProperty note = new VObjectProperty("NOTE", "¡Hola, mundo!"); 144 * note.getParameters().put(null, "QUOTED-PRINTABLE"); 145 * vobjectWriter.writeProperty(note); 146 * </pre> 147 * <p> 148 * By default, the property value is encoded under the UTF-8 character set when 149 * encoded in quoted-printable encoding. This can be changed by specifying a 150 * CHARSET parameter. If the character set is not recognized by the local JVM, 151 * then UTF-8 will be used. 152 * </p> 153 * 154 * <pre class="brush:java"> 155 * StringWriter sw = new StringWriter(); 156 * VObjectWriter vobjectWriter = new VObjectWriter(sw, ...); 157 * 158 * VObjectProperty note = new VObjectProperty("NOTE", "¡Hola, mundo!"); 159 * note.getParameters().put("ENCODING", "QUOTED-PRINTABLE"); 160 * note.getParameters().put("CHARSET", "Windows-1252"); 161 * vobjectWriter.writeProperty(note); 162 * vobjectWriter.close(); 163 * 164 * assertEquals("NOTE;ENCODING=QUOTED-PRINTABLE;CHARSET=Windows-1252:=A1Hola, mundo!\r\n", sw.toString()); 165 * </pre> 166 * 167 * <p> 168 * <b>Circumflex Accent Encoding</b> 169 * </p> 170 * <p> 171 * Newlines and double quote characters are not permitted inside of parameter 172 * values unless circumflex accent encoding is enabled. It is turned off by 173 * default. 174 * </p> 175 * <p> 176 * Note that this encoding mechanism is defined in a separate specification and 177 * may not be supported by the consumer of the vobject data. Also note that it 178 * can only be used with new-style syntax. 179 * </p> 180 * 181 * <pre class="brush:java"> 182 * StringWriter sw = new StringWriter(); 183 * VObjectWriter vobjectWriter = new VObjectWriter(sw, SyntaxStyle.NEW); 184 * vobjectWriter.setCaretEncodingEnabled(true); 185 * 186 * VObjectProperty note = new VObjectProperty("NOTE", "The truth is out there."); 187 * note.getParameters().put("X-AUTHOR", "Fox \"Spooky\" Mulder"); 188 * vobjectWriter.writeProperty(note); 189 * vobjectWriter.close(); 190 * 191 * assertEquals("NOTE;X-AUTHOR=Fox ^'Spooky^' Mulder:The truth is out there.\r\n", sw.toString()); 192 * </pre> 193 * 194 * <p> 195 * <b>Line Folding</b> 196 * </p> 197 * <p> 198 * Lines longer than 75 characters are automatically folded, as per the 199 * vCard/iCalendar recommendation. 200 * </p> 201 * 202 * <pre class="brush:java"> 203 * StringWriter sw = new StringWriter(); 204 * VObjectWriter vobjectWriter = new VObjectWriter(sw, ...); 205 * 206 * vobjectWriter.writeProperty("NOTE", "Lorem ipsum dolor sit amet\, consectetur adipiscing elit. Vestibulum ultricies tempor orci ac dignissim."); 207 * vobjectWriter.close(); 208 * 209 * assertEquals( 210 * "NOTE:Lorem ipsum dolor sit amet\\, consectetur adipiscing elit. Vestibulum u\r\n" + 211 * " ltricies tempor orci ac dignissim.\r\n" 212 * , sw.toString()); 213 * </pre> 214 * <p> 215 * The line folding length can be adjusted to a length of your choosing. In 216 * addition, passing in a "null" line length will disable line folding. 217 * </p> 218 * 219 * <pre class="brush:java"> 220 * StringWriter sw = new StringWriter(); 221 * VObjectWriter vobjectWriter = new VObjectWriter(sw, ...); 222 * vobjectWriter.getFoldedLineWriter().setLineLength(null); 223 * 224 * vobjectWriter.writeProperty("NOTE", "Lorem ipsum dolor sit amet\, consectetur adipiscing elit. Vestibulum ultricies tempor orci ac dignissim."); 225 * vobjectWriter.close(); 226 * 227 * assertEquals("NOTE:Lorem ipsum dolor sit amet\\, consectetur adipiscing elit. Vestibulum ultricies tempor orci ac dignissim.\r\n", sw.toString()); 228 * </pre> 229 * 230 * <p> 231 * You may also specify what kind of folding whitespace to use. The default is a 232 * single space character, but this can be changed to any combination of tabs 233 * and spaces. Note that new-style syntax requires the folding whitespace to be 234 * EXACTLY ONE character long. 235 * </p> 236 * 237 * <pre class="brush:java"> 238 * StringWriter sw = new StringWriter(); 239 * VObjectWriter vobjectWriter = new VObjectWriter(sw, ...); 240 * vobjectWriter.getFoldedLineWriter().setIndent("\t"); 241 * 242 * vobjectWriter.writeProperty("NOTE", "Lorem ipsum dolor sit amet\, consectetur adipiscing elit. Vestibulum ultricies tempor orci ac dignissim."); 243 * vobjectWriter.close(); 244 * 245 * assertEquals( 246 * "NOTE:Lorem ipsum dolor sit amet\\, consectetur adipiscing elit. Vestibulum u\r\n" + 247 * "\tltricies tempor orci ac dignissim.\r\n" 248 * , sw.toString()); 249 * </pre> 250 * @author Michael Angstadt 251 */ 252public class VObjectWriter implements Closeable, Flushable { 253 private final FoldedLineWriter writer; 254 private boolean caretEncodingEnabled = false; 255 private SyntaxStyle syntaxStyle; 256 257 private final AllowedCharacters allowedPropertyNameChars; 258 private final AllowedCharacters allowedGroupChars; 259 private final AllowedCharacters allowedParameterNameChars; 260 private AllowedCharacters allowedParameterValueChars; 261 262 /** 263 * Creates a new vobject writer. 264 * @param writer the output stream 265 * @param syntaxStyle the syntax style to use 266 */ 267 public VObjectWriter(Writer writer, SyntaxStyle syntaxStyle) { 268 this.writer = new FoldedLineWriter(writer); 269 this.syntaxStyle = syntaxStyle; 270 271 allowedGroupChars = VObjectValidator.allowedCharactersGroup(syntaxStyle, false); 272 allowedPropertyNameChars = VObjectValidator.allowedCharactersPropertyName(syntaxStyle, false); 273 allowedParameterNameChars = VObjectValidator.allowedCharactersParameterName(syntaxStyle, false); 274 allowedParameterValueChars = VObjectValidator.allowedCharactersParameterValue(syntaxStyle, false, false); 275 } 276 277 /** 278 * Gets the writer that is used to write data to the output stream. 279 * @return the folded line writer 280 */ 281 public FoldedLineWriter getFoldedLineWriter() { 282 return writer; 283 } 284 285 /** 286 * <p> 287 * Gets whether the writer will apply circumflex accent encoding on 288 * parameter values (disabled by default). This escaping mechanism allows 289 * for newlines and double quotes to be included in parameter values. It is 290 * only supported by new style syntax. 291 * </p> 292 * 293 * <table class="simpleTable"> 294 * <caption>Characters encoded by circumflex accent encoding</caption> 295 * <tr> 296 * <th>Raw Character</th> 297 * <th>Encoded Character</th> 298 * </tr> 299 * <tr> 300 * <td>{@code "}</td> 301 * <td>{@code ^'}</td> 302 * </tr> 303 * <tr> 304 * <td><i>newline</i></td> 305 * <td>{@code ^n}</td> 306 * </tr> 307 * <tr> 308 * <td>{@code ^}</td> 309 * <td>{@code ^^}</td> 310 * </tr> 311 * </table> 312 * 313 * <p> 314 * Example: 315 * </p> 316 * 317 * <pre> 318 * GEO;X-ADDRESS="Pittsburgh Pirates^n115 Federal St^nPittsburgh, PA 15212":40.446816;80.00566 319 * </pre> 320 * 321 * @return true if circumflex accent encoding is enabled, false if not 322 * @see <a href="http://tools.ietf.org/html/rfc6868">RFC 6868</a> 323 */ 324 public boolean isCaretEncodingEnabled() { 325 return caretEncodingEnabled; 326 } 327 328 /** 329 * <p> 330 * Sets whether the writer will apply circumflex accent encoding on 331 * parameter values (disabled by default). This escaping mechanism allows 332 * for newlines and double quotes to be included in parameter values. It is 333 * only supported by new style syntax. 334 * </p> 335 * 336 * <table class="simpleTable"> 337 * <caption>Characters encoded by circumflex accent encoding</caption> 338 * <tr> 339 * <th>Raw Character</th> 340 * <th>Encoded Character</th> 341 * </tr> 342 * <tr> 343 * <td>{@code "}</td> 344 * <td>{@code ^'}</td> 345 * </tr> 346 * <tr> 347 * <td><i>newline</i></td> 348 * <td>{@code ^n}</td> 349 * </tr> 350 * <tr> 351 * <td>{@code ^}</td> 352 * <td>{@code ^^}</td> 353 * </tr> 354 * </table> 355 * 356 * <p> 357 * Example: 358 * </p> 359 * 360 * <pre> 361 * GEO;X-ADDRESS="Pittsburgh Pirates^n115 Federal St^nPittsburgh, PA 15212":40.446816;80.00566 362 * </pre> 363 * 364 * @param enable true to use circumflex accent encoding, false not to 365 * @see <a href="http://tools.ietf.org/html/rfc6868">RFC 6868</a> 366 */ 367 public void setCaretEncodingEnabled(boolean enable) { 368 caretEncodingEnabled = enable; 369 allowedParameterValueChars = VObjectValidator.allowedCharactersParameterValue(syntaxStyle, enable, false); 370 } 371 372 /** 373 * Gets the syntax style the writer is using. 374 * @return the syntax style 375 */ 376 public SyntaxStyle getSyntaxStyle() { 377 return syntaxStyle; 378 } 379 380 /** 381 * Sets the syntax style that the writer should use. 382 * @param syntaxStyle the syntax style 383 */ 384 public void setSyntaxStyle(SyntaxStyle syntaxStyle) { 385 this.syntaxStyle = syntaxStyle; 386 } 387 388 /** 389 * Writes a property marking the beginning of a component. 390 * @param componentName the component name (e.g. "VCARD") 391 * @throws IllegalArgumentException if the component name is null or empty 392 * @throws IOException if there's a problem writing to the data stream 393 */ 394 public void writeBeginComponent(String componentName) throws IOException { 395 if (componentName == null || componentName.length() == 0) { 396 throw new IllegalArgumentException("Component name cannot be null or empty."); 397 } 398 writeProperty("BEGIN", componentName); 399 } 400 401 /** 402 * Writes a property marking the end of a component. 403 * @param componentName the component name (e.g. "VCARD") 404 * @throws IllegalArgumentException if the component name is null or empty 405 * @throws IOException if there's a problem writing to the data stream 406 */ 407 public void writeEndComponent(String componentName) throws IOException { 408 if (componentName == null || componentName.length() == 0) { 409 throw new IllegalArgumentException("Component name cannot be null or empty."); 410 } 411 writeProperty("END", componentName); 412 } 413 414 /** 415 * Writes a "VERSION" property. 416 * @param version the version string (e.g. "2.1") 417 * @throws IllegalArgumentException if the version string is null or empty 418 * @throws IOException if there's a problem writing to the data stream 419 */ 420 public void writeVersion(String version) throws IOException { 421 if (version == null || version.length() == 0) { 422 throw new IllegalArgumentException("Version string cannot be null or empty."); 423 } 424 writeProperty("VERSION", version); 425 } 426 427 /** 428 * Writes a property to the data stream. 429 * @param name the property name (e.g. "FN") 430 * @param value the property value 431 * @throws IllegalArgumentException if the given data contains one or more 432 * characters which would break the syntax and cannot be written 433 * @throws IOException if there's a problem writing to the data stream 434 */ 435 public void writeProperty(String name, String value) throws IOException { 436 writeProperty(null, name, new VObjectParameters(), value); 437 } 438 439 /** 440 * Writes a property to the data stream. 441 * @param property the property to write 442 * @throws IllegalArgumentException if the given data contains one or more 443 * characters which would break the syntax and cannot be written 444 * @throws IOException if there's a problem writing to the data stream 445 */ 446 public void writeProperty(VObjectProperty property) throws IOException { 447 writeProperty(property.getGroup(), property.getName(), property.getParameters(), property.getValue()); 448 } 449 450 /** 451 * Writes a property to the data stream. 452 * @param group the group or null if there is no group 453 * @param name the property name (e.g. "FN") 454 * @param parameters the property parameters 455 * @param value the property value (will be converted to "quoted-printable" 456 * encoding if the ENCODING parameter is set to "QUOTED-PRINTABLE") 457 * @throws IllegalArgumentException if the given data contains one or more 458 * characters which would break the syntax and cannot be written 459 * @throws IOException if there's a problem writing to the data stream 460 */ 461 public void writeProperty(String group, String name, VObjectParameters parameters, String value) throws IOException { 462 /* 463 * Ensure that the property is safe to write before writing it. 464 */ 465 validate(group, name, parameters); 466 467 parametersCopied = false; 468 469 if (value == null) { 470 value = ""; 471 } 472 473 //sanitize value 474 switch (syntaxStyle) { 475 case OLD: 476 /* 477 * Old style does not support the "\n" escape sequence so encode the 478 * value in quoted-printable encoding if any newline characters 479 * exist. 480 */ 481 if (containsNewlines(value) && !parameters.isQuotedPrintable()) { 482 parameters = copyParameters(parameters); 483 parameters.put("ENCODING", "QUOTED-PRINTABLE"); 484 } 485 break; 486 case NEW: 487 value = escapeNewlines(value); 488 break; 489 } 490 491 /* 492 * Determine if the property value must be encoded in quoted printable 493 * encoding. If so, then determine what character set to use for the 494 * encoding. 495 */ 496 boolean useQuotedPrintable = parameters.isQuotedPrintable(); 497 Charset quotedPrintableCharset = null; 498 if (useQuotedPrintable) { 499 try { 500 quotedPrintableCharset = parameters.getCharset(); 501 } catch (Exception e) { 502 //character set not recognized 503 } 504 505 if (quotedPrintableCharset == null) { 506 quotedPrintableCharset = Charset.forName("UTF-8"); 507 parameters = copyParameters(parameters); 508 parameters.replace("CHARSET", quotedPrintableCharset.name()); 509 } 510 } 511 512 //write the group 513 if (group != null && !group.isEmpty()) { 514 writer.append(group).append('.'); 515 } 516 517 //write the property name 518 writer.append(name); 519 520 //write the parameters 521 for (Map.Entry<String, List<String>> parameter : parameters) { 522 String parameterName = parameter.getKey(); 523 List<String> parameterValues = parameter.getValue(); 524 if (parameterValues.isEmpty()) { 525 continue; 526 } 527 528 if (syntaxStyle == SyntaxStyle.OLD) { 529 //e.g. ADR;TYPE=home;TYPE=work;TYPE=another,value: 530 531 for (String parameterValue : parameterValues) { 532 parameterValue = sanitizeOldStyleParameterValue(parameterValue); 533 534 writer.append(';'); 535 if (parameterName != null) { 536 writer.append(parameterName).append('='); 537 } 538 writer.append(parameterValue); 539 } 540 } else { 541 //e.g. ADR;TYPE=home,work,"another,value": 542 543 writer.append(';'); 544 if (parameterName != null) { 545 writer.append(parameterName).append('='); 546 } 547 548 boolean first = true; 549 for (String parameterValue : parameterValues) { 550 parameterValue = sanitizeNewStyleParameterValue(parameterValue); 551 552 if (!first) { 553 writer.append(','); 554 } 555 556 if (shouldQuoteParameterValue(parameterValue)) { 557 writer.append('"').append(parameterValue).append('"'); 558 } else { 559 writer.append(parameterValue); 560 } 561 562 first = false; 563 } 564 } 565 } 566 567 writer.append(':'); 568 writer.write(value, useQuotedPrintable, quotedPrintableCharset); 569 writer.writeln(); 570 } 571 572 /** 573 * Checks to make sure the given property data is safe to write (does not 574 * contain illegal characters, etc). 575 * @param group the property group or null if not set 576 * @param name the property name 577 * @param parameters the property parameters 578 * @throws IllegalArgumentException if there is a validation error 579 */ 580 private void validate(String group, String name, VObjectParameters parameters) { 581 //validate the group name 582 if (group != null) { 583 if (!allowedGroupChars.check(group)) { 584 throw new IllegalArgumentException("Property \"" + name + "\" has its group set to \"" + group + "\". This group name contains one or more invalid characters. The following characters are not permitted: " + allowedGroupChars.flip()); 585 } 586 if (beginsWithWhitespace(group)) { 587 throw new IllegalArgumentException("Property \"" + name + "\" has its group set to \"" + group + "\". This group name begins with one or more whitespace characters, which is not permitted."); 588 } 589 } 590 591 //validate the property name 592 if (name.isEmpty()) { 593 throw new IllegalArgumentException("Property name cannot be empty."); 594 } 595 if (!allowedPropertyNameChars.check(name)) { 596 throw new IllegalArgumentException("Property name \"" + name + "\" contains one or more invalid characters. The following characters are not permitted: " + allowedPropertyNameChars.flip()); 597 } 598 if (beginsWithWhitespace(name)) { 599 throw new IllegalArgumentException("Property name \"" + name + "\" begins with one or more whitespace characters, which is not permitted."); 600 } 601 602 //validate the parameter names and values 603 for (Map.Entry<String, List<String>> parameter : parameters) { 604 //validate the parameter name 605 String parameterName = parameter.getKey(); 606 if (parameterName == null && syntaxStyle == SyntaxStyle.NEW) { 607 throw new IllegalArgumentException("Property \"" + name + "\" has a parameter whose name is null. This is not permitted with new style syntax."); 608 } 609 if (parameterName != null && !allowedParameterNameChars.check(parameterName)) { 610 throw new IllegalArgumentException("Property \"" + name + "\" has a parameter named \"" + parameterName + "\". This parameter's name contains one or more invalid characters. The following characters are not permitted: " + allowedParameterNameChars.flip()); 611 } 612 613 //validate the parameter values 614 List<String> parameterValues = parameter.getValue(); 615 for (String parameterValue : parameterValues) { 616 if (!allowedParameterValueChars.check(parameterValue)) { 617 throw new IllegalArgumentException("Property \"" + name + "\" has a parameter named \"" + parameterName + "\" whose value contains one or more invalid characters. The following characters are not permitted: " + allowedParameterValueChars.flip()); 618 } 619 } 620 } 621 } 622 623 /** 624 * Determines if a string contains at least one newline character. 625 * @param string the string 626 * @return true if it contains at least one newline character, false if not 627 */ 628 private boolean containsNewlines(String string) { 629 for (int i = 0; i < string.length(); i++) { 630 char c = string.charAt(i); 631 switch (c) { 632 case '\r': 633 case '\n': 634 return true; 635 } 636 } 637 return false; 638 } 639 640 /** 641 * Determines if a parameter value should be enclosed in double quotes. 642 * @param value the parameter value 643 * @return true if it should be enclosed in double quotes, false if not 644 */ 645 private boolean shouldQuoteParameterValue(String value) { 646 for (int i = 0; i < value.length(); i++) { 647 char c = value.charAt(i); 648 switch (c) { 649 case ',': 650 case ':': 651 case ';': 652 return true; 653 } 654 } 655 return false; 656 } 657 658 /** 659 * Determines if a string starts with whitespace. 660 * @param string the string 661 * @return true if it starts with whitespace, false if not 662 */ 663 private boolean beginsWithWhitespace(String string) { 664 if (string.length() == 0) { 665 return false; 666 } 667 char first = string.charAt(0); 668 return (first == ' ' || first == '\t'); 669 } 670 671 /** 672 * <p> 673 * Sanitizes a parameter value for new style syntax. 674 * </p> 675 * <p> 676 * This method applies circumflex accent encoding, if it's enabled. 677 * Otherwise, it returns the value unchanged. 678 * </p> 679 * @param value the parameter value 680 * @return the sanitized parameter value 681 */ 682 private String sanitizeNewStyleParameterValue(String value) { 683 if (caretEncodingEnabled) { 684 return applyCaretEncoding(value); 685 } 686 687 return value; 688 } 689 690 /** 691 * <p> 692 * Sanitizes a parameter value for old style syntax. 693 * </p> 694 * <p> 695 * This method escapes backslashes and semicolons. 696 * </p> 697 * @param value the parameter value 698 * @return the sanitized parameter value 699 */ 700 private String sanitizeOldStyleParameterValue(String value) { 701 StringBuilder sb = null; 702 for (int i = 0; i < value.length(); i++) { 703 char c = value.charAt(i); 704 705 if (c == '\\' || c == ';') { 706 if (sb == null) { 707 sb = new StringBuilder(value.length() * 2); 708 sb.append(value, 0, i); 709 } 710 sb.append('\\'); 711 } 712 713 if (sb != null) { 714 sb.append(c); 715 } 716 } 717 return (sb == null) ? value : sb.toString(); 718 } 719 720 /** 721 * Applies circumflex accent encoding to a parameter value. 722 * @param value the parameter value 723 * @return the encoded value 724 */ 725 private String applyCaretEncoding(String value) { 726 StringBuilder sb = null; 727 char prev = 0; 728 for (int i = 0; i < value.length(); i++) { 729 char c = value.charAt(i); 730 731 if (c == '^' || c == '"' || c == '\r' || c == '\n') { 732 if (c == '\n' && prev == '\r') { 733 /* 734 * Do not write a second newline escape sequence if the 735 * newline sequence is "\r\n". 736 */ 737 } else { 738 if (sb == null) { 739 sb = new StringBuilder(value.length() * 2); 740 sb.append(value, 0, i); 741 } 742 sb.append('^'); 743 744 switch (c) { 745 case '\r': 746 case '\n': 747 sb.append('n'); 748 break; 749 case '"': 750 sb.append('\''); 751 break; 752 default: 753 sb.append(c); 754 } 755 } 756 } else if (sb != null) { 757 sb.append(c); 758 } 759 760 prev = c; 761 } 762 return (sb == null) ? value : sb.toString(); 763 } 764 765 private boolean parametersCopied; 766 767 /** 768 * Copies the given list of parameters if it hasn't been copied before. 769 * @param parameters the parameters 770 * @return the copy or the same object if the parameters were copied before 771 */ 772 private VObjectParameters copyParameters(VObjectParameters parameters) { 773 if (parametersCopied) { 774 return parameters; 775 } 776 777 VObjectParameters copy = new VObjectParameters(parameters); 778 parametersCopied = true; 779 return copy; 780 } 781 782 /** 783 * Flushes the underlying output stream. 784 * @throws IOException if there's a problem flushing the output stream 785 */ 786 public void flush() throws IOException { 787 writer.flush(); 788 } 789 790 /** 791 * Closes the underlying output stream. 792 * @throws IOException if there's a problem closing the output stream 793 */ 794 public void close() throws IOException { 795 writer.close(); 796 } 797}