001    package ezvcard.io;
002    
003    import java.io.Closeable;
004    import java.io.File;
005    import java.io.FileWriter;
006    import java.io.IOException;
007    import java.io.OutputStream;
008    import java.io.OutputStreamWriter;
009    import java.io.StringWriter;
010    import java.io.Writer;
011    import java.util.ArrayList;
012    import java.util.Arrays;
013    import java.util.BitSet;
014    import java.util.HashMap;
015    import java.util.List;
016    import java.util.Map;
017    import java.util.regex.Pattern;
018    
019    import org.apache.commons.codec.EncoderException;
020    import org.apache.commons.codec.net.QuotedPrintableCodec;
021    
022    import ezvcard.VCard;
023    import ezvcard.VCardSubTypes;
024    import ezvcard.VCardVersion;
025    import ezvcard.parameters.AddressTypeParameter;
026    import ezvcard.parameters.EncodingParameter;
027    import ezvcard.parameters.TypeParameter;
028    import ezvcard.types.AddressType;
029    import ezvcard.types.LabelType;
030    import ezvcard.types.MemberType;
031    import ezvcard.types.ProdIdType;
032    import ezvcard.types.TextType;
033    import ezvcard.types.VCardType;
034    import ezvcard.util.VCardStringUtils;
035    
036    /*
037     Copyright (c) 2012, Michael Angstadt
038     All rights reserved.
039    
040     Redistribution and use in source and binary forms, with or without
041     modification, are permitted provided that the following conditions are met: 
042    
043     1. Redistributions of source code must retain the above copyright notice, this
044     list of conditions and the following disclaimer. 
045     2. Redistributions in binary form must reproduce the above copyright notice,
046     this list of conditions and the following disclaimer in the documentation
047     and/or other materials provided with the distribution. 
048    
049     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
050     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
051     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
052     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
053     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
054     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
055     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
056     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
057     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
058     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
059    
060     The views and conclusions contained in the software and documentation are those
061     of the authors and should not be interpreted as representing official policies, 
062     either expressed or implied, of the FreeBSD Project.
063     */
064    
065    /**
066     * Converts vCards to string representations.
067     * @author Michael Angstadt
068     */
069    public class VCardWriter implements Closeable {
070            private static final Pattern quoteMeRegex = Pattern.compile(".*?[,:;].*");
071            private static final Pattern newlineRegex = Pattern.compile("\\r\\n|\\r|\\n");
072    
073            /**
074             * The characters that are not valid in parameter values and that should be
075             * removed.
076             */
077            private static final Map<VCardVersion, BitSet> invalidParamValueChars = new HashMap<VCardVersion, BitSet>();
078            static {
079                    BitSet controlChars = new BitSet(128);
080                    controlChars.set(0, 31);
081                    controlChars.set(127);
082                    controlChars.set('\t', false); //allow
083                    controlChars.set('\n', false); //allow
084                    controlChars.set('\r', false); //allow
085    
086                    //2.1
087                    {
088                            BitSet bitSet = new BitSet(128);
089                            bitSet.or(controlChars);
090    
091                            bitSet.set(',');
092                            bitSet.set('.');
093                            bitSet.set(':');
094                            bitSet.set('=');
095                            bitSet.set('[');
096                            bitSet.set(']');
097    
098                            invalidParamValueChars.put(VCardVersion.V2_1, bitSet);
099                    }
100    
101                    //3.0, 4.0
102                    {
103                            BitSet bitSet = new BitSet(128);
104                            bitSet.or(controlChars);
105    
106                            invalidParamValueChars.put(VCardVersion.V3_0, bitSet);
107                            invalidParamValueChars.put(VCardVersion.V4_0, bitSet);
108                    }
109            }
110    
111            private CompatibilityMode compatibilityMode = CompatibilityMode.RFC;
112            private VCardVersion targetVersion = VCardVersion.V3_0;
113            private String newline;
114            private boolean addProdId = true;
115            private boolean caretEncodingEnabled = false;
116            private FoldingScheme foldingScheme;
117            private List<String> warnings = new ArrayList<String>();
118            private final Writer writer;
119    
120            /**
121             * Creates a vCard writer (writes v3.0 vCards and uses the standard folding
122             * scheme and newline sequence).
123             * @param out the output stream to write the vCard to
124             */
125            public VCardWriter(OutputStream out) {
126                    this(new OutputStreamWriter(out));
127            }
128    
129            /**
130             * Creates a vCard writer (uses the standard folding scheme and newline
131             * sequence).
132             * @param out the output stream to write the vCard to
133             * @param targetVersion the version that the vCards should conform to
134             */
135            public VCardWriter(OutputStream out, VCardVersion targetVersion) {
136                    this(new OutputStreamWriter(out), targetVersion);
137            }
138    
139            /**
140             * Creates a vCard writer.
141             * @param out the output stream to write the vCard to
142             * @param targetVersion the version that the vCards should conform to
143             * @param foldingScheme the folding scheme to use or null not to fold at all
144             * @param newline the newline sequence to use
145             */
146            public VCardWriter(OutputStream out, VCardVersion targetVersion, FoldingScheme foldingScheme, String newline) {
147                    this(new OutputStreamWriter(out), targetVersion, foldingScheme, newline);
148            }
149    
150            /**
151             * Creates a vCard writer (writes v3.0 vCards and uses the standard folding
152             * scheme and newline sequence).
153             * @param file the file to write the vCard to
154             * @throws IOException if there's a problem opening the file
155             */
156            public VCardWriter(File file) throws IOException {
157                    this(new FileWriter(file));
158            }
159    
160            /**
161             * Creates a vCard writer (uses the standard folding scheme and newline
162             * sequence).
163             * @param file the file to write the vCard to
164             * @param targetVersion the version that the vCards should conform to
165             * @throws IOException if there's a problem opening the file
166             */
167            public VCardWriter(File file, VCardVersion targetVersion) throws IOException {
168                    this(new FileWriter(file), targetVersion);
169            }
170    
171            /**
172             * Creates a vCard writer.
173             * @param file the file to write the vCard to
174             * @param targetVersion the version that the vCards should conform to
175             * @param foldingScheme the folding scheme to use or null not to fold at all
176             * @param newline the newline sequence to use
177             * @throws IOException if there's a problem opening the file
178             */
179            public VCardWriter(File file, VCardVersion targetVersion, FoldingScheme foldingScheme, String newline) throws IOException {
180                    this(new FileWriter(file), targetVersion, foldingScheme, newline);
181            }
182    
183            /**
184             * Creates a vCard writer (writes v3.0 vCards and uses the standard folding
185             * scheme and newline sequence).
186             * @param writer the writer to write the vCard to
187             */
188            public VCardWriter(Writer writer) {
189                    this(writer, VCardVersion.V3_0);
190            }
191    
192            /**
193             * Creates a vCard writer (uses the standard folding scheme and newline
194             * sequence).
195             * @param writer the writer to write the vCard to
196             * @param targetVersion the version that the vCards should conform to
197             */
198            public VCardWriter(Writer writer, VCardVersion targetVersion) {
199                    this(writer, targetVersion, FoldingScheme.MIME_DIR, "\r\n");
200            }
201    
202            /**
203             * Creates a vCard writer.
204             * @param writer the writer to write the vCard to
205             * @param targetVersion the version that the vCards should conform to
206             * @param foldingScheme the folding scheme to use or null not to fold at all
207             * @param newline the newline sequence to use
208             */
209            public VCardWriter(Writer writer, VCardVersion targetVersion, FoldingScheme foldingScheme, String newline) {
210                    if (writer instanceof FoldedLineWriter || foldingScheme == null) {
211                            //the check for FoldedLineWriter is for writing nested 2.1 vCards (i.e. the AGENT type)
212                            this.writer = writer;
213                    } else {
214                            this.writer = new FoldedLineWriter(writer, foldingScheme.getLineLength(), foldingScheme.getIndent(), newline);
215                    }
216                    this.targetVersion = targetVersion;
217                    this.foldingScheme = foldingScheme;
218                    this.newline = newline;
219            }
220    
221            /**
222             * Gets the compatibility mode. Used for customizing the marshalling process
223             * to target a particular application.
224             * @return the compatibility mode
225             */
226            @Deprecated
227            public CompatibilityMode getCompatibilityMode() {
228                    return compatibilityMode;
229            }
230    
231            /**
232             * Sets the compatibility mode. Used for customizing the marshalling process
233             * to target a particular application.
234             * @param compatibilityMode the compatibility mode
235             */
236            @Deprecated
237            public void setCompatibilityMode(CompatibilityMode compatibilityMode) {
238                    this.compatibilityMode = compatibilityMode;
239            }
240    
241            /**
242             * Gets the version that the vCards should adhere to.
243             * @return the vCard version
244             */
245            public VCardVersion getTargetVersion() {
246                    return targetVersion;
247            }
248    
249            /**
250             * Sets the version that the vCards should adhere to.
251             * @param targetVersion the vCard version
252             */
253            public void setTargetVersion(VCardVersion targetVersion) {
254                    this.targetVersion = targetVersion;
255            }
256    
257            /**
258             * Gets whether or not a "PRODID" type will be added to each vCard, saying
259             * that the vCard was generated by this library. For 2.1 vCards, the
260             * extended type "X-PRODID" will be added, since "PRODID" is not supported
261             * by that version.
262             * @return true if it will be added, false if not (defaults to true)
263             */
264            public boolean isAddProdId() {
265                    return addProdId;
266            }
267    
268            /**
269             * Sets whether or not to add a "PRODID" type to each vCard, saying that the
270             * vCard was generated by this library. For 2.1 vCards, the extended type
271             * "X-PRODID" will be added, since "PRODID" is not supported by that
272             * version.
273             * @param addProdId true to add this type, false not to (defaults to true)
274             */
275            public void setAddProdId(boolean addProdId) {
276                    this.addProdId = addProdId;
277            }
278    
279            /**
280             * <p>
281             * Gets whether the writer will use circumflex accent encoding for vCard 3.0
282             * and 4.0 parameter values. This escaping mechanism allows for newlines and
283             * double quotes to be included in parameter values.
284             * </p>
285             * 
286             * <table border="1">
287             * <tr>
288             * <th>Character</th>
289             * <th>Replacement</th>
290             * </tr>
291             * <tr>
292             * <td><code>"</code></td>
293             * <td><code>^'</code></td>
294             * </tr>
295             * <tr>
296             * <td><i>newline</i></td>
297             * <td><code>^n</code></td>
298             * </tr>
299             * <tr>
300             * <td><code>^</code></td>
301             * <td><code>^^</code></td>
302             * </tr>
303             * </table>
304             * 
305             * <p>
306             * This setting is disabled by default and is only used with 3.0 and 4.0
307             * vCards. When writing a vCard with this setting disabled, newlines will be
308             * escaped as "\n", backslashes will be escaped as "\\", and double quotes
309             * will be replaced with single quotes.
310             * </p>
311             * 
312             * <p>
313             * Example:
314             * </p>
315             * 
316             * <pre>
317             * GEO;X-ADDRESS="Pittsburgh Pirates^n115 Federal St^nPitt
318             *  sburgh, PA 15212":geo: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 use circumflex accent encoding for vCard 3.0
331             * and 4.0 parameter values. This escaping mechanism allows for newlines and
332             * double quotes to be included in parameter values.
333             * </p>
334             * 
335             * <table border="1">
336             * <tr>
337             * <th>Character</th>
338             * <th>Replacement</th>
339             * </tr>
340             * <tr>
341             * <td><code>"</code></td>
342             * <td><code>^'</code></td>
343             * </tr>
344             * <tr>
345             * <td><i>newline</i></td>
346             * <td><code>^n</code></td>
347             * </tr>
348             * <tr>
349             * <td><code>^</code></td>
350             * <td><code>^^</code></td>
351             * </tr>
352             * </table>
353             * 
354             * <p>
355             * This setting is disabled by default and is only used with 3.0 and 4.0
356             * vCards. When writing a vCard with this setting disabled, newlines will be
357             * escaped as "\n", backslashes will be escaped as "\\", and double quotes
358             * will be replaced with single quotes.
359             * </p>
360             * 
361             * <p>
362             * Example:
363             * </p>
364             * 
365             * <pre>
366             * GEO;X-ADDRESS="Pittsburgh Pirates^n115 Federal St^nPitt
367             *  sburgh, PA 15212":geo:40.446816,-80.00566
368             * </pre>
369             * 
370             * @param enable true to use circumflex accent encoding, false not to
371             * @see <a href="http://tools.ietf.org/html/rfc6868">RFC 6868</a>
372             */
373            public void setCaretEncodingEnabled(boolean enable) {
374                    caretEncodingEnabled = enable;
375            }
376    
377            /**
378             * Gets the newline sequence that is used to separate lines.
379             * @return the newline sequence
380             */
381            public String getNewline() {
382                    return newline;
383            }
384    
385            /**
386             * Gets the rules for how each line is folded.
387             * @return the folding scheme or null if the lines are not folded
388             */
389            public FoldingScheme getFoldingScheme() {
390                    return foldingScheme;
391            }
392    
393            /**
394             * Gets the warnings from the last vCard that was marshalled. This list is
395             * reset every time a new vCard is written.
396             * @return the warnings or empty list if there were no warnings
397             */
398            public List<String> getWarnings() {
399                    return new ArrayList<String>(warnings);
400            }
401    
402            /**
403             * Writes a vCard
404             * @param vcard the vCard to write
405             * @throws IOException if there's a problem writing to the output stream
406             */
407            public void write(final VCard vcard) throws IOException {
408                    warnings.clear();
409    
410                    if (targetVersion == VCardVersion.V2_1 || targetVersion == VCardVersion.V3_0) {
411                            if (vcard.getStructuredName() == null) {
412                                    warnings.add("vCard version " + targetVersion + " requires that a structured name be defined.");
413                            }
414                    }
415    
416                    if (targetVersion == VCardVersion.V3_0 || targetVersion == VCardVersion.V4_0) {
417                            if (vcard.getFormattedName() == null) {
418                                    warnings.add("vCard version " + targetVersion + " requires that a formatted name be defined.");
419                            }
420                    }
421    
422                    List<VCardType> typesToAdd = new ArrayList<VCardType>();
423                    typesToAdd.add(new TextType("BEGIN", "VCARD"));
424                    typesToAdd.add(new TextType("VERSION", targetVersion.getVersion()));
425    
426                    for (VCardType type : vcard) {
427                            if (addProdId && type instanceof ProdIdType) {
428                                    //do not add the PRODID in the vCard if "addProdId" is true
429                                    continue;
430                            }
431    
432                            //determine if this type is supported by the target version
433                            if (!supportsTargetVersion(type)) {
434                                    warnings.add(type.getTypeName() + " is not supported by vCard version " + targetVersion + " and will not be added to the vCard.  Supported versions are: " + Arrays.toString(type.getSupportedVersions()));
435                                    continue;
436                            }
437    
438                            //check for correct KIND value if there are MEMBER types
439                            if (type instanceof MemberType && (vcard.getKind() == null || !vcard.getKind().isGroup())) {
440                                    warnings.add("KIND must be set to \"group\" in order to add MEMBER properties to the vCard.");
441                                    continue;
442                            }
443    
444                            typesToAdd.add(type);
445    
446                            //add LABEL types for each ADR type if the target version is 2.1 or 3.0
447                            if (type instanceof AddressType && targetVersion != VCardVersion.V4_0) {
448                                    AddressType adr = (AddressType) type;
449                                    String labelStr = adr.getLabel();
450                                    if (labelStr != null) {
451                                            LabelType label = new LabelType(labelStr);
452                                            for (AddressTypeParameter t : adr.getTypes()) {
453                                                    label.addType(t);
454                                            }
455                                            typesToAdd.add(label);
456                                    }
457                            }
458                    }
459    
460                    //add an extended type saying it was generated by this library
461                    if (addProdId) {
462                            EzvcardProdIdType prodId = new EzvcardProdIdType(targetVersion);
463                            typesToAdd.add(prodId);
464                    }
465    
466                    typesToAdd.add(new TextType("END", "VCARD"));
467    
468                    List<String> warningsBuf = new ArrayList<String>();
469                    for (VCardType type : typesToAdd) {
470                            //marshal the value
471                            warningsBuf.clear();
472                            String value = null;
473                            VCard nested = null;
474                            try {
475                                    value = type.marshalText(targetVersion, warningsBuf, compatibilityMode);
476                            } catch (SkipMeException e) {
477                                    warningsBuf.add(type.getTypeName() + " property will not be marshalled: " + e.getMessage());
478                                    continue;
479                            } catch (EmbeddedVCardException e) {
480                                    nested = e.getVCard();
481                            } finally {
482                                    warnings.addAll(warningsBuf);
483                            }
484    
485                            //marshal the sub types
486                            warningsBuf.clear();
487                            VCardSubTypes subTypes;
488                            try {
489                                    subTypes = type.marshalSubTypes(targetVersion, warningsBuf, compatibilityMode, vcard);
490                            } finally {
491                                    warnings.addAll(warningsBuf);
492                            }
493    
494                            //sanitize value for safe inclusion in the vCard
495                            if (value != null) {
496                                    if (targetVersion == VCardVersion.V2_1) {
497                                            if (VCardStringUtils.containsNewlines(value)) {
498                                                    //2.1 does not support the "\n" escape sequence (see "Delimiters" sub-section in section 2 of the specs)
499                                                    QuotedPrintableCodec codec = new QuotedPrintableCodec();
500                                                    try {
501                                                            value = codec.encode(value);
502                                                            subTypes.setEncoding(EncodingParameter.QUOTED_PRINTABLE);
503                                                    } catch (EncoderException e) {
504                                                            warnings.add("A unexpected error occurred while encoding the value of the " + type.getTypeName() + " property in \"quoted-printable\" encoding.  Value will not be encoded.\n" + e.getMessage());
505                                                            value = VCardStringUtils.escapeNewlines(value);
506                                                    }
507                                            }
508                                    } else {
509                                            value = VCardStringUtils.escapeNewlines(value);
510                                    }
511                            }
512    
513                            StringBuilder sb = new StringBuilder();
514    
515                            //write the group
516                            if (type.getGroup() != null) {
517                                    sb.append(type.getGroup());
518                                    sb.append('.');
519                            }
520    
521                            //write the type name
522                            sb.append(type.getTypeName());
523    
524                            //write the Sub Types
525                            for (String subTypeName : subTypes.getNames()) {
526                                    List<String> subTypeValues = subTypes.get(subTypeName);
527                                    if (!subTypeValues.isEmpty()) {
528                                            if (targetVersion == VCardVersion.V2_1) {
529                                                    boolean typeSubType = TypeParameter.NAME.equalsIgnoreCase(subTypeName);
530                                                    for (String subTypeValue : subTypeValues) {
531                                                            subTypeValue = sanitizeSubTypeValue(subTypeValue, subTypeName, type.getTypeName());
532    
533                                                            if (typeSubType) {
534                                                                    //e.g. ADR;HOME;WORK:
535                                                                    sb.append(';').append(subTypeValue.toUpperCase());
536                                                            } else {
537                                                                    //e.g. ADR;FOO=bar;FOO=car:
538                                                                    sb.append(';').append(subTypeName).append('=').append(subTypeValue);
539                                                            }
540                                                    }
541                                            } else {
542                                                    //e.g. ADR;TYPE=home,work,"another,value":
543    
544                                                    sb.append(';').append(subTypeName).append('=');
545                                                    for (String subTypeValue : subTypeValues) {
546                                                            subTypeValue = sanitizeSubTypeValue(subTypeValue, subTypeName, type.getTypeName());
547    
548                                                            //surround with double quotes if contains special chars
549                                                            if (quoteMeRegex.matcher(subTypeValue).matches()) {
550                                                                    sb.append('"');
551                                                                    sb.append(subTypeValue);
552                                                                    sb.append('"');
553                                                            } else {
554                                                                    sb.append(subTypeValue);
555                                                            }
556    
557                                                            sb.append(',');
558                                                    }
559                                                    sb.deleteCharAt(sb.length() - 1); //chomp last comma
560                                            }
561                                    }
562                            }
563    
564                            sb.append(':');
565    
566                            writer.write(sb.toString());
567    
568                            //write the value
569                            if (nested == null) {
570                                    writer.write(value);
571                                    writer.write(newline);
572                            } else {
573                                    if (targetVersion == VCardVersion.V2_1) {
574                                            writer.write(newline);
575    
576                                            //write a nested vCard (2.1 style)
577                                            VCardWriter agentWriter = new VCardWriter(writer, targetVersion);
578                                            agentWriter.setAddProdId(false);
579                                            agentWriter.setCompatibilityMode(compatibilityMode);
580                                            try {
581                                                    agentWriter.write(nested);
582                                            } finally {
583                                                    for (String w : agentWriter.getWarnings()) {
584                                                            warnings.add(type.getTypeName() + " marshal warning: " + w);
585                                                    }
586                                            }
587                                    } else {
588                                            //write an embedded vCard (3.0 style)
589                                            StringWriter sw = new StringWriter();
590                                            VCardWriter agentWriter = new VCardWriter(sw, targetVersion, null, "\n");
591                                            agentWriter.setAddProdId(false);
592                                            agentWriter.setCompatibilityMode(compatibilityMode);
593                                            try {
594                                                    agentWriter.write(nested);
595                                            } finally {
596                                                    for (String w : agentWriter.getWarnings()) {
597                                                            warnings.add("Problem marshalling nested vCard for " + type.getTypeName() + ": " + w);
598                                                    }
599                                            }
600    
601                                            String vCardStr = sw.toString();
602                                            vCardStr = VCardStringUtils.escape(vCardStr);
603                                            vCardStr = VCardStringUtils.escapeNewlines(vCardStr);
604                                            writer.write(vCardStr);
605                                            writer.write(newline);
606                                    }
607                            }
608                    }
609            }
610    
611            private String sanitizeSubTypeValue(String value, String subTypeName, String typeName) {
612                    String modifiedValue = null;
613                    boolean subTypeValueChanged = false;
614    
615                    switch (targetVersion) {
616                    case V2_1:
617                            //remove invalid characters
618                            modifiedValue = removeInvalidSubTypeValueChars(value);
619    
620                            //replace newlines with spaces
621                            modifiedValue = newlineRegex.matcher(modifiedValue).replaceAll(" ");
622    
623                            //check to see if value was changed
624                            subTypeValueChanged = (value != modifiedValue);
625    
626                            //escape backslashes
627                            modifiedValue = modifiedValue.replace("\\", "\\\\");
628    
629                            //escape semi-colons (see section 2)
630                            modifiedValue = modifiedValue.replace(";", "\\;");
631    
632                            break;
633    
634                    case V3_0:
635                    case V4_0:
636                            //remove invalid characters
637                            modifiedValue = removeInvalidSubTypeValueChars(value);
638    
639                            if (caretEncodingEnabled) {
640                                    subTypeValueChanged = (modifiedValue != value);
641                                    modifiedValue = applyCaretEncoding(modifiedValue);
642                            } else {
643                                    //replace double quotes with single quotes
644                                    modifiedValue = modifiedValue.replace('"', '\'');
645                                    subTypeValueChanged = (modifiedValue != value);
646    
647                                    //escape backslashes
648                                    modifiedValue = modifiedValue.replace("\\", "\\\\");
649    
650                                    //escape newlines
651                                    if (targetVersion == VCardVersion.V3_0) {
652                                            modifiedValue = newlineRegex.matcher(modifiedValue).replaceAll(" ");
653                                    } else {
654                                            //for the "LABEL" parameter
655                                            modifiedValue = newlineRegex.matcher(modifiedValue).replaceAll("\\\\\\n");
656                                    }
657                            }
658    
659                            break;
660                    }
661    
662                    if (subTypeValueChanged) {
663                            warnings.add("Parameter \"" + subTypeName + "\" of property \"" + typeName + "\" contained one or more characters which are not allowed in vCard " + targetVersion.getVersion() + " parameter values.  These characters were removed.");
664                    }
665    
666                    return modifiedValue;
667            }
668    
669            private String removeInvalidSubTypeValueChars(String value) {
670                    BitSet invalidChars = invalidParamValueChars.get(targetVersion);
671                    StringBuilder sb = new StringBuilder(value.length());
672    
673                    for (int i = 0; i < value.length(); i++) {
674                            char ch = value.charAt(i);
675                            if (!invalidChars.get(ch)) {
676                                    sb.append(ch);
677                            }
678                    }
679    
680                    return (sb.length() == value.length()) ? value : sb.toString();
681            }
682    
683            private String applyCaretEncoding(String value) {
684                    value = value.replace("^", "^^");
685                    value = newlineRegex.matcher(value).replaceAll("^n");
686                    value = value.replace("\"", "^'");
687                    return value;
688            }
689    
690            private boolean supportsTargetVersion(VCardType type) {
691                    for (VCardVersion version : type.getSupportedVersions()) {
692                            if (version == targetVersion) {
693                                    return true;
694                            }
695                    }
696                    return false;
697            }
698    
699            /**
700             * Closes the underlying {@link Writer} object.
701             */
702            public void close() throws IOException {
703                    writer.close();
704            }
705    }