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}