001package ezvcard.io.text;
002
003import java.io.IOException;
004import java.io.InputStream;
005import java.io.InputStreamReader;
006import java.io.Reader;
007import java.io.StringReader;
008import java.nio.charset.Charset;
009import java.nio.file.Files;
010import java.nio.file.Path;
011import java.util.ArrayList;
012import java.util.List;
013import java.util.Optional;
014
015import com.github.mangstadt.vinnie.VObjectProperty;
016import com.github.mangstadt.vinnie.io.Context;
017import com.github.mangstadt.vinnie.io.SyntaxRules;
018import com.github.mangstadt.vinnie.io.VObjectDataListener;
019import com.github.mangstadt.vinnie.io.VObjectPropertyValues;
020import com.github.mangstadt.vinnie.io.VObjectReader;
021import com.github.mangstadt.vinnie.io.Warning;
022
023import ezvcard.VCard;
024import ezvcard.VCardDataType;
025import ezvcard.VCardVersion;
026import ezvcard.io.CannotParseException;
027import ezvcard.io.EmbeddedVCardException;
028import ezvcard.io.ParseWarning;
029import ezvcard.io.SkipMeException;
030import ezvcard.io.StreamReader;
031import ezvcard.io.scribe.RawPropertyScribe;
032import ezvcard.io.scribe.VCardPropertyScribe;
033import ezvcard.parameter.Encoding;
034import ezvcard.parameter.VCardParameters;
035import ezvcard.property.Address;
036import ezvcard.property.Label;
037import ezvcard.property.VCardProperty;
038import ezvcard.util.IOUtils;
039import ezvcard.util.StringUtils;
040
041/*
042 Copyright (c) 2012-2026, Michael Angstadt
043 All rights reserved.
044
045 Redistribution and use in source and binary forms, with or without
046 modification, are permitted provided that the following conditions are met: 
047
048 1. Redistributions of source code must retain the above copyright notice, this
049 list of conditions and the following disclaimer. 
050 2. Redistributions in binary form must reproduce the above copyright notice,
051 this list of conditions and the following disclaimer in the documentation
052 and/or other materials provided with the distribution. 
053
054 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
055 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
056 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
057 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
058 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
059 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
060 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
061 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
062 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
063 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
064
065 The views and conclusions contained in the software and documentation are those
066 of the authors and should not be interpreted as representing official policies, 
067 either expressed or implied, of the FreeBSD Project.
068 */
069
070/**
071 * <p>
072 * Parses {@link VCard} objects from a plain-text vCard data stream.
073 * </p>
074 * <p>
075 * <b>Example:</b>
076 * </p>
077 * 
078 * <pre class="brush:java">
079 * Path file = Paths.get("vcards.vcf");
080 * try (VCardReader reader = new VCardReader(file)) {
081 *   VCard vcard;
082 *   while ((vcard = reader.readNext()) != null) {
083 *     //...
084 *   }
085 * }
086 * </pre>
087 * @author Michael Angstadt
088 * @see <a href="http://www.imc.org/pdi/vcard-21.rtf">vCard 2.1</a>
089 * @see <a href="http://tools.ietf.org/html/rfc2426">RFC 2426 (3.0)</a>
090 * @see <a href="http://tools.ietf.org/html/rfc6350">RFC 6350 (4.0)</a>
091 */
092public class VCardReader extends StreamReader {
093        private final VObjectReader reader;
094        private final VCardVersion defaultVersion;
095
096        /**
097         * Creates a new vCard reader.
098         * @param str the string to read from
099         */
100        public VCardReader(String str) {
101                this(str, VCardVersion.V2_1);
102        }
103
104        /**
105         * Creates a new vCard reader.
106         * @param str the string to read from
107         * @param defaultVersion the version to assume the vCard is in until a
108         * VERSION property is encountered (defaults to 2.1)
109         */
110        public VCardReader(String str, VCardVersion defaultVersion) {
111                this(new StringReader(str), defaultVersion);
112        }
113
114        /**
115         * Creates a new vCard reader.
116         * @param in the input stream to read from
117         */
118        public VCardReader(InputStream in) {
119                this(in, VCardVersion.V2_1);
120        }
121
122        /**
123         * Creates a new vCard reader.
124         * @param in the input stream to read from
125         * @param defaultVersion the version to assume the vCard is in until a
126         * VERSION property is encountered (defaults to 2.1)
127         */
128        public VCardReader(InputStream in, VCardVersion defaultVersion) {
129                this(new InputStreamReader(in), defaultVersion);
130        }
131
132        /**
133         * Creates a new vCard reader.
134         * @param file the file to read from
135         * @throws IOException if there is a problem opening the file
136         */
137        public VCardReader(Path file) throws IOException {
138                this(file, VCardVersion.V2_1);
139        }
140
141        /**
142         * Creates a new vCard reader.
143         * @param file the file to read from
144         * @param defaultVersion the version to assume the vCard is in until a
145         * VERSION property is encountered (defaults to 2.1)
146         * @throws IOException if there is a problem opening the file
147         */
148        public VCardReader(Path file, VCardVersion defaultVersion) throws IOException {
149                this(Files.newBufferedReader(file), defaultVersion);
150        }
151
152        /**
153         * Creates a new vCard reader.
154         * @param reader the reader to read from
155         */
156        public VCardReader(Reader reader) {
157                this(reader, VCardVersion.V2_1);
158        }
159
160        /**
161         * Creates a new vCard reader.
162         * @param reader the reader to read from
163         * @param defaultVersion the version to assume the vCard is in until a
164         * VERSION property is encountered (defaults to 2.1)
165         */
166        public VCardReader(Reader reader, VCardVersion defaultVersion) {
167                SyntaxRules rules = SyntaxRules.vcard();
168                rules.setDefaultSyntaxStyle(defaultVersion.getSyntaxStyle());
169                this.reader = new VObjectReader(reader, rules);
170                this.defaultVersion = defaultVersion;
171        }
172
173        /**
174         * Gets whether the reader will decode parameter values that use circumflex
175         * accent encoding (enabled by default). This escaping mechanism allows
176         * newlines and double quotes to be included in parameter values.
177         * @return true if circumflex accent decoding is enabled, false if not
178         * @see VObjectReader#isCaretDecodingEnabled()
179         */
180        public boolean isCaretDecodingEnabled() {
181                return reader.isCaretDecodingEnabled();
182        }
183
184        /**
185         * Sets whether the reader will decode parameter values that use circumflex
186         * accent encoding (enabled by default). This escaping mechanism allows
187         * newlines and double quotes to be included in parameter values.
188         * @param enable true to use circumflex accent decoding, false not to
189         * @see VObjectReader#setCaretDecodingEnabled(boolean)
190         */
191        public void setCaretDecodingEnabled(boolean enable) {
192                reader.setCaretDecodingEnabled(enable);
193        }
194
195        /**
196         * Gets the character set to use when the parser cannot determine what
197         * character set to use to decode a quoted-printable property value.
198         * @return the character set
199         * @see VObjectReader#getDefaultQuotedPrintableCharset()
200         */
201        public Charset getDefaultQuotedPrintableCharset() {
202                return reader.getDefaultQuotedPrintableCharset();
203        }
204
205        /**
206         * Sets the character set to use when the parser cannot determine what
207         * character set to use to decode a quoted-printable property value.
208         * @param charset the character set (cannot be null)
209         * @see VObjectReader#setDefaultQuotedPrintableCharset
210         */
211        public void setDefaultQuotedPrintableCharset(Charset charset) {
212                reader.setDefaultQuotedPrintableCharset(charset);
213        }
214
215        @Override
216        protected VCard _readNext() throws IOException {
217                VObjectDataListenerImpl listener = new VObjectDataListenerImpl();
218                reader.parse(listener);
219                return listener.root;
220        }
221
222        private class VObjectDataListenerImpl implements VObjectDataListener {
223                private VCard root;
224                private final VCardStack stack = new VCardStack();
225                private EmbeddedVCardException embeddedVCardException;
226
227                public void onComponentBegin(String name, Context context) {
228                        if (!isVCardComponent(name)) {
229                                //ignore non-VCARD components
230                                return;
231                        }
232
233                        VCard vcard = new VCard(defaultVersion);
234                        if (stack.isEmpty()) {
235                                root = vcard;
236                        }
237                        stack.push(vcard);
238
239                        if (embeddedVCardException != null) {
240                                embeddedVCardException.injectVCard(vcard);
241                                embeddedVCardException = null;
242                        }
243                }
244
245                public void onComponentEnd(String name, Context context) {
246                        if (!isVCardComponent(name)) {
247                                //ignore non-VCARD components
248                                return;
249                        }
250
251                        VCardStack.Item item = stack.pop();
252                        assignLabels(item.vcard, item.labels);
253
254                        /*
255                         * Is the version property completely absent?
256                         * 
257                         * If a VERSION property with an invalid value is present, the
258                         * property is parsed as a RawProperty and a different warning is
259                         * generated by vinnie.
260                         */
261                        if (!item.validVersionPropertyFound && item.vcard.getExtendedProperty("VERSION") == null) {
262                                //@formatter:off
263                                warnings.add(new ParseWarning.Builder()
264                                        .message(39, item.vcard.getVersion())
265                                        .build()
266                                );
267                                //@formatter:on
268                        }
269
270                        if (stack.isEmpty()) {
271                                context.stop();
272                        }
273                }
274
275                public void onProperty(VObjectProperty vobjectProperty, Context vobjectContext) {
276                        if (!inVCardComponent(vobjectContext.getParentComponents())) {
277                                //ignore properties that are not directly inside a VCARD component
278                                return;
279                        }
280
281                        if (embeddedVCardException != null) {
282                                //the next property was supposed to be the start of a nested vCard, but it wasn't
283                                embeddedVCardException.injectVCard(null);
284                                embeddedVCardException = null;
285                        }
286
287                        VCard curVCard = stack.peek().vcard;
288                        VCardVersion version = curVCard.getVersion();
289
290                        VCardProperty property = parseProperty(vobjectProperty, version, vobjectContext.getLineNumber());
291                        if (property != null) {
292                                curVCard.addProperty(property);
293                        }
294                }
295
296                private VCardProperty parseProperty(VObjectProperty vobjectProperty, VCardVersion version, int lineNumber) {
297                        String group = vobjectProperty.getGroup();
298                        String name = vobjectProperty.getName();
299                        VCardParameters parameters = new VCardParameters(vobjectProperty.getParameters().getMap());
300                        String value = vobjectProperty.getValue();
301
302                        context.getWarnings().clear();
303                        context.setVersion(version);
304                        context.setLineNumber(lineNumber);
305                        context.setPropertyName(name);
306
307                        //sanitize the parameters
308                        processNamelessParameters(parameters);
309                        processQuotedMultivaluedTypeParams(parameters, version);
310
311                        //get the scribe
312                        VCardPropertyScribe<? extends VCardProperty> scribe = index.getPropertyScribe(name);
313                        if (scribe == null) {
314                                scribe = new RawPropertyScribe(name);
315                        }
316
317                        //get the data type (VALUE parameter)
318                        VCardDataType dataType = parameters.getValue();
319                        parameters.setValue(null);
320                        if (dataType == null) {
321                                //use the default data type if there is no VALUE parameter
322                                dataType = scribe.defaultDataType(version);
323                        }
324
325                        VCardProperty property;
326                        try {
327                                property = scribe.parseText(value, dataType, parameters, context);
328                                warnings.addAll(context.getWarnings());
329                        } catch (SkipMeException e) {
330                                handleSkippedProperty(name, lineNumber, e);
331                                return null;
332                        } catch (CannotParseException e) {
333                                property = handleUnparseableProperty(name, parameters, value, dataType, lineNumber, version, e);
334                        } catch (EmbeddedVCardException e) {
335                                handledEmbeddedVCard(name, value, lineNumber, e);
336                                property = e.getProperty();
337                        }
338
339                        property.setGroup(group);
340
341                        /*
342                         * LABEL properties must be treated specially so they can be matched
343                         * up with the ADR properties that they belong to. LABELs are not
344                         * added to the vCard as properties, they are added to the ADR
345                         * properties they belong to (unless they cannot be matched up with
346                         * an ADR).
347                         */
348                        if (property instanceof Label) {
349                                Label label = (Label) property;
350                                stack.peek().labels.add(label);
351                                return null;
352                        }
353
354                        handleLabelParameter(property);
355
356                        return property;
357                }
358
359                private void handleSkippedProperty(String propertyName, int lineNumber, SkipMeException e) {
360                        //@formatter:off
361                        warnings.add(new ParseWarning.Builder(context)
362                                .message(22, e.getMessage())
363                                .build()
364                        );
365                        //@formatter:on
366                }
367
368                private VCardProperty handleUnparseableProperty(String name, VCardParameters parameters, String value, VCardDataType dataType, int lineNumber, VCardVersion version, CannotParseException e) {
369                        //@formatter:off
370                        warnings.add(new ParseWarning.Builder(context)
371                                .message(e)
372                                .build()
373                        );
374                        //@formatter:on
375
376                        RawPropertyScribe scribe = new RawPropertyScribe(name);
377                        return scribe.parseText(value, dataType, parameters, null);
378                }
379
380                private void handledEmbeddedVCard(String name, String value, int lineNumber, EmbeddedVCardException exception) {
381                        /*
382                         * If the property does not have a value, a nested vCard is expected
383                         * to be next (2.1 style).
384                         */
385                        if (value.trim().isEmpty()) {
386                                embeddedVCardException = exception;
387                                return;
388                        }
389
390                        /*
391                         * If the property does have a value, the property value should be
392                         * an embedded vCard (3.0 style).
393                         */
394                        value = VObjectPropertyValues.unescape(value);
395
396                        VCardReader agentReader = new VCardReader(value);
397                        agentReader.setCaretDecodingEnabled(isCaretDecodingEnabled());
398                        agentReader.setDefaultQuotedPrintableCharset(getDefaultQuotedPrintableCharset());
399                        agentReader.setScribeIndex(index);
400
401                        try {
402                                VCard nestedVCard = agentReader.readNext();
403                                if (nestedVCard != null) {
404                                        exception.injectVCard(nestedVCard);
405                                }
406                        } catch (IOException ignore) {
407                                //shouldn't be thrown because we're reading from a string
408                        } finally {
409                                warnings.addAll(agentReader.getWarnings());
410                                IOUtils.closeQuietly(agentReader);
411                        }
412                }
413
414                /**
415                 * <p>
416                 * Unescapes newline sequences in the LABEL parameter of {@link Address}
417                 * properties. Newlines cannot normally be escaped in parameter values.
418                 * </p>
419                 * <p>
420                 * Only version 4.0 allows this (and only version 4.0 defines a LABEL
421                 * parameter), but do this for all versions for compatibility.
422                 * </p>
423                 * @param property the property
424                 */
425                private void handleLabelParameter(VCardProperty property) {
426                        if (!(property instanceof Address)) {
427                                return;
428                        }
429
430                        Address adr = (Address) property;
431                        String label = adr.getLabel();
432                        if (label == null) {
433                                return;
434                        }
435
436                        label = label.replace("\\n", StringUtils.NEWLINE);
437                        adr.setLabel(label);
438                }
439
440                public void onVersion(String value, Context vobjectContext) {
441                        VCardVersion version = VCardVersion.valueOfByStr(value);
442                        context.setVersion(version);
443
444                        VCardStack.Item item = stack.peek();
445                        item.vcard.setVersion(version);
446                        item.validVersionPropertyFound = true;
447                }
448
449                public void onWarning(Warning warning, VObjectProperty property, Exception thrown, Context vobjectContext) {
450                        if (!inVCardComponent(vobjectContext.getParentComponents())) {
451                                //ignore warnings that are not directly inside a VCARD component
452                                return;
453                        }
454
455                        //@formatter:off
456                        warnings.add(new ParseWarning.Builder(context)
457                                .lineNumber(vobjectContext.getLineNumber())
458                                .propertyName((property == null) ? null : property.getName())
459                                .message(27, warning.getMessage(), vobjectContext.getUnfoldedLine())
460                                .build()
461                        );
462                        //@formatter:on
463                }
464
465                private boolean inVCardComponent(List<String> parentComponents) {
466                        if (parentComponents.isEmpty()) {
467                                return false;
468                        }
469                        String last = parentComponents.get(parentComponents.size() - 1);
470                        return isVCardComponent(last);
471                }
472
473                private boolean isVCardComponent(String componentName) {
474                        return "VCARD".equals(componentName);
475                }
476
477                /**
478                 * Assigns names to all nameless parameters. v3.0 and v4.0 require all
479                 * parameters to have names, but v2.1 does not.
480                 * @param parameters the parameters
481                 */
482                private void processNamelessParameters(VCardParameters parameters) {
483                        List<String> namelessValues = parameters.removeAll(null);
484                        for (String value : namelessValues) {
485                                String name = guessParameterName(value);
486                                parameters.put(name, value);
487                        }
488                }
489
490                /**
491                 * Makes a guess as to what a parameter value's name should be.
492                 * @param value the parameter value (e.g. "HOME")
493                 * @return the guessed name (e.g. "TYPE")
494                 */
495                private String guessParameterName(String value) {
496                        if (VCardDataType.find(value) != null) {
497                                return VCardParameters.VALUE;
498                        }
499
500                        if (Encoding.find(value) != null) {
501                                return VCardParameters.ENCODING;
502                        }
503
504                        //otherwise, assume it's a TYPE
505                        return VCardParameters.TYPE;
506                }
507
508                /**
509                 * <p>
510                 * Accounts for multi-valued TYPE parameters being enclosed entirely in
511                 * double quotes (for example: ADR;TYPE="home,work").
512                 * </p>
513                 * <p>
514                 * Many examples throughout the 4.0 specs show TYPE parameters being
515                 * encoded in this way. This conflicts with the ABNF and is noted in the
516                 * errata. This method will parse these incorrectly-formatted TYPE
517                 * parameters as if they were multi-valued, even though, technically,
518                 * they are not.
519                 * </p>
520                 * @param parameters the parameters
521                 * @param version the version
522                 */
523                private void processQuotedMultivaluedTypeParams(VCardParameters parameters, VCardVersion version) {
524                        if (version == VCardVersion.V2_1) {
525                                return;
526                        }
527
528                        List<String> types = parameters.getTypes();
529                        if (types.isEmpty()) {
530                                return;
531                        }
532
533                        //@formatter:off
534                        Optional<String> valueWithCommaOpt = types.stream()
535                                .filter(value -> value.indexOf(',') >= 0)
536                        .findFirst();
537                        //@formatter:on
538
539                        if (!valueWithCommaOpt.isPresent()) {
540                                return;
541                        }
542
543                        String valueWithComma = valueWithCommaOpt.get();
544                        types.clear();
545                        int prev = -1;
546                        int cur;
547                        while ((cur = valueWithComma.indexOf(',', prev + 1)) >= 0) {
548                                types.add(valueWithComma.substring(prev + 1, cur));
549                                prev = cur;
550                        }
551                        types.add(valueWithComma.substring(prev + 1));
552                }
553        }
554
555        /**
556         * Keeps track of the hierarchy of nested vCards.
557         */
558        private static class VCardStack {
559                private final List<Item> stack = new ArrayList<>();
560
561                /**
562                 * Adds a vCard to the stack.
563                 * @param vcard the vcard to add
564                 */
565                public void push(VCard vcard) {
566                        stack.add(new Item(vcard, new ArrayList<>()));
567                }
568
569                /**
570                 * Removes the top item from the stack and returns it.
571                 * @return the last item or null if the stack is empty
572                 */
573                public Item pop() {
574                        return isEmpty() ? null : stack.remove(stack.size() - 1);
575                }
576
577                /**
578                 * Gets the top item of the stack.
579                 * @return the top item
580                 */
581                public Item peek() {
582                        return isEmpty() ? null : stack.get(stack.size() - 1);
583                }
584
585                /**
586                 * Determines if the stack is empty.
587                 * @return true if it's empty, false if not
588                 */
589                public boolean isEmpty() {
590                        return stack.isEmpty();
591                }
592
593                private static class Item {
594                        public final VCard vcard;
595                        public final List<Label> labels;
596                        public boolean validVersionPropertyFound = false;
597
598                        public Item(VCard vcard, List<Label> labels) {
599                                this.vcard = vcard;
600                                this.labels = labels;
601                        }
602                }
603        }
604
605        /**
606         * Closes the input stream.
607         * @throws IOException if there's a problem closing the input stream
608         */
609        public void close() throws IOException {
610                reader.close();
611        }
612}