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