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