001    package ezvcard.io;
002    
003    import java.io.Closeable;
004    import java.io.File;
005    import java.io.FileNotFoundException;
006    import java.io.FileReader;
007    import java.io.IOException;
008    import java.io.InputStream;
009    import java.io.InputStreamReader;
010    import java.io.Reader;
011    import java.io.StringReader;
012    import java.lang.reflect.Method;
013    import java.util.ArrayList;
014    import java.util.Arrays;
015    import java.util.HashMap;
016    import java.util.List;
017    import java.util.Map;
018    
019    import org.apache.commons.codec.DecoderException;
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.EncodingParameter;
026    import ezvcard.parameters.TypeParameter;
027    import ezvcard.parameters.ValueParameter;
028    import ezvcard.types.AddressType;
029    import ezvcard.types.LabelType;
030    import ezvcard.types.RawType;
031    import ezvcard.types.TypeList;
032    import ezvcard.types.VCardType;
033    import ezvcard.util.VCardStringUtils;
034    
035    /*
036     Copyright (c) 2012, Michael Angstadt
037     All rights reserved.
038    
039     Redistribution and use in source and binary forms, with or without
040     modification, are permitted provided that the following conditions are met: 
041    
042     1. Redistributions of source code must retain the above copyright notice, this
043     list of conditions and the following disclaimer. 
044     2. Redistributions in binary form must reproduce the above copyright notice,
045     this list of conditions and the following disclaimer in the documentation
046     and/or other materials provided with the distribution. 
047    
048     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
049     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
050     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
051     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
052     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
053     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
054     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
055     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
056     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
057     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
058    
059     The views and conclusions contained in the software and documentation are those
060     of the authors and should not be interpreted as representing official policies, 
061     either expressed or implied, of the FreeBSD Project.
062     */
063    
064    /**
065     * Unmarshals vCards into {@link VCard} objects.
066     * @author Michael Angstadt
067     */
068    public class VCardReader implements Closeable, IParser {
069            private CompatibilityMode compatibilityMode = CompatibilityMode.RFC;
070            private List<String> warnings = new ArrayList<String>();
071            private Map<String, Class<? extends VCardType>> extendedTypeClasses = new HashMap<String, Class<? extends VCardType>>();
072            private FoldedLineReader reader;
073            private boolean caretDecodingEnabled = true;
074    
075            /**
076             * @param str the string to read the vCards from
077             */
078            public VCardReader(String str) {
079                    this(new StringReader(str));
080            }
081    
082            /**
083             * @param in the input stream to read the vCards from
084             */
085            public VCardReader(InputStream in) {
086                    this(new InputStreamReader(in));
087            }
088    
089            /**
090             * @param file the file to read the vCards from
091             * @throws FileNotFoundException if the file doesn't exist
092             */
093            public VCardReader(File file) throws FileNotFoundException {
094                    this(new FileReader(file));
095            }
096    
097            /**
098             * @param reader the reader to read the vCards from
099             */
100            public VCardReader(Reader reader) {
101                    this.reader = new FoldedLineReader(reader);
102            }
103    
104            /**
105             * This constructor is used for reading embedded 2.1 vCards (see 2.1 docs,
106             * p.19)
107             * @param reader the reader to read the vCards from
108             */
109            private VCardReader(FoldedLineReader reader) {
110                    this.reader = reader;
111            }
112    
113            /**
114             * <p>
115             * Gets whether the reader will decode characters in parameter values that
116             * use circumflex accent encoding. This escaping mechanism allows for
117             * newlines and double quotes to be included in parameter values.
118             * </p>
119             * 
120             * <table border="1">
121             * <tr>
122             * <th>Raw</th>
123             * <th>Encoded</th>
124             * </tr>
125             * <tr>
126             * <td><code>"</code></td>
127             * <td><code>^'</code></td>
128             * </tr>
129             * <tr>
130             * <td><i>newline</i></td>
131             * <td><code>^n</code></td>
132             * </tr>
133             * <tr>
134             * <td><code>^</code></td>
135             * <td><code>^^</code></td>
136             * </tr>
137             * </table>
138             * 
139             * <p>
140             * This setting is enabled by default and is only used with 3.0 and 4.0
141             * vCards.
142             * </p>
143             * 
144             * <p>
145             * Example:
146             * </p>
147             * 
148             * <pre>
149             * GEO;X-ADDRESS="Pittsburgh Pirates^n115 Federal St^nPitt
150             *  sburgh, PA 15212":geo:40.446816,-80.00566
151             * </pre>
152             * 
153             * @return true if circumflex accent decoding is enabled, false if not
154             * @see <a href="http://tools.ietf.org/html/rfc6868">RFC 6868</a>
155             */
156            public boolean isCaretDecodingEnabled() {
157                    return caretDecodingEnabled;
158            }
159    
160            /**
161             * <p>
162             * Sets whether the reader will decode characters in parameter values that
163             * use circumflex accent encoding. This escaping mechanism allows for
164             * newlines and double quotes to be included in parameter values.
165             * </p>
166             * 
167             * <table border="1">
168             * <tr>
169             * <th>Raw</th>
170             * <th>Encoded</th>
171             * </tr>
172             * <tr>
173             * <td><code>"</code></td>
174             * <td><code>^'</code></td>
175             * </tr>
176             * <tr>
177             * <td><i>newline</i></td>
178             * <td><code>^n</code></td>
179             * </tr>
180             * <tr>
181             * <td><code>^</code></td>
182             * <td><code>^^</code></td>
183             * </tr>
184             * </table>
185             * 
186             * <p>
187             * This setting is enabled by default and is only used with 3.0 and 4.0
188             * vCards.
189             * </p>
190             * 
191             * <p>
192             * Example:
193             * </p>
194             * 
195             * <pre>
196             * GEO;X-ADDRESS="Pittsburgh Pirates^n115 Federal St^nPitt
197             *  sburgh, PA 15212":geo:40.446816,-80.00566
198             * </pre>
199             * 
200             * @param enable true to use circumflex accent decoding, false not to
201             * @see <a href="http://tools.ietf.org/html/rfc6868">RFC 6868</a>
202             */
203            public void setCaretDecodingEnabled(boolean enable) {
204                    caretDecodingEnabled = enable;
205            }
206    
207            /**
208             * Gets the compatibility mode. Used for customizing the unmarshalling
209             * process based on the application that generated the vCard.
210             * @return the compatibility mode
211             */
212            @Deprecated
213            public CompatibilityMode getCompatibilityMode() {
214                    return compatibilityMode;
215            }
216    
217            /**
218             * Sets the compatibility mode. Used for customizing the unmarshalling
219             * process based on the application that generated the vCard.
220             * @param compatibilityMode the compatibility mode
221             */
222            @Deprecated
223            public void setCompatibilityMode(CompatibilityMode compatibilityMode) {
224                    this.compatibilityMode = compatibilityMode;
225            }
226    
227            //@Override
228            public void registerExtendedType(Class<? extends VCardType> clazz) {
229                    extendedTypeClasses.put(getTypeNameFromTypeClass(clazz), clazz);
230            }
231    
232            //@Override
233            public void unregisterExtendedType(Class<? extends VCardType> clazz) {
234                    extendedTypeClasses.remove(getTypeNameFromTypeClass(clazz));
235            }
236    
237            //@Override
238            public List<String> getWarnings() {
239                    return new ArrayList<String>(warnings);
240            }
241    
242            //@Override
243            public VCard readNext() throws IOException {
244                    warnings.clear();
245    
246                    VCard vcard = new VCard();
247    
248                    List<LabelType> labels = new ArrayList<LabelType>();
249                    VCardVersion version = null;
250                    boolean endFound = false;
251                    int typesRead = 0;
252                    String line;
253                    List<String> warningsBuf = new ArrayList<String>();
254    
255                    while (!endFound && (line = reader.readLine()) != null) {
256                            //parse the components out of the line
257                            VCardLine parsedLine = VCardLine.parse(line, version, caretDecodingEnabled);
258                            if (parsedLine == null) {
259                                    warnings.add("Skipping malformed vCard line: \"" + line + "\"");
260                                    continue;
261                            }
262    
263                            //build the sub types 
264                            VCardSubTypes subTypes = new VCardSubTypes();
265                            for (List<String> subType : parsedLine.getSubTypes()) {
266                                    //if the parameter is name-less, make a guess at what the name is
267                                    //v3.0 and v4.0 requires all sub types to have names, but v2.1 does not
268                                    String subTypeName = subType.get(0);
269                                    List<String> subTypeValues = subType.subList(1, subType.size());
270                                    if (subTypeName == null) {
271                                            String subTypeValue = subTypeValues.get(0);
272                                            if (ValueParameter.valueOf(subTypeValue) != null) {
273                                                    subTypeName = ValueParameter.NAME;
274                                            } else if (EncodingParameter.valueOf(subTypeValue) != null) {
275                                                    subTypeName = EncodingParameter.NAME;
276                                            } else {
277                                                    //otherwise, assume it's a TYPE
278                                                    subTypeName = TypeParameter.NAME;
279                                            }
280    
281                                            subTypes.put(subTypeName, subTypeValue);
282                                    } else {
283                                            subTypeName = subTypeName.toUpperCase();
284    
285                                            //account for multi-valued TYPE parameters being enclosed entirely in double quotes
286                                            //e.g. ADR;TYPE="home,work"
287                                            if (subTypeName.equals(TypeParameter.NAME) && subTypeValues.size() == 1) {
288                                                    //@formatter:off
289                                                    /*
290                                                     * Many examples throughout the 4.0 specs show TYPE parameters being encoded in this way.
291                                                     * This conflicts with the ABNF and is noted in the errata.
292                                                     * Split the value by comma incase the vendor implemented it this way.
293                                                     */
294                                                    //@formatter:on
295                                                    subTypeValues = Arrays.asList(subTypeValues.get(0).split(","));
296                                            }
297    
298                                            for (String subTypeValue : subTypeValues) {
299                                                    subTypes.put(subTypeName, subTypeValue);
300                                            }
301                                    }
302                            }
303    
304                            String typeName = parsedLine.getTypeName().toUpperCase();
305                            String value = VCardStringUtils.ltrim(parsedLine.getValue());
306                            String groupName = parsedLine.getGroup();
307    
308                            //if the value is encoded in "quoted-printable", decode it
309                            //"quoted-printable" encoding is only supported in v2.1
310                            if (subTypes.getEncoding() == EncodingParameter.QUOTED_PRINTABLE) {
311                                    QuotedPrintableCodec codec = new QuotedPrintableCodec();
312                                    String charset = subTypes.getCharset();
313                                    try {
314                                            value = (charset == null) ? codec.decode(value) : codec.decode(value, charset);
315                                    } catch (DecoderException e) {
316                                            warnings.add("The value of the " + typeName + " type was marked as \"quoted-printable\", but it could not be decoded.  Assuming that the value is plain text.");
317                                    }
318                                    subTypes.setEncoding(null); //remove encoding sub type
319                            }
320    
321                            //vCard should start with "BEGIN"
322                            if (typesRead == 0 && !"BEGIN".equals(typeName)) {
323                                    warnings.add("vCard does not start with \"BEGIN\".");
324                            }
325    
326                            typesRead++;
327    
328                            if ("BEGIN".equals(typeName)) {
329                                    if (!"vcard".equalsIgnoreCase(value)) {
330                                            warnings.add("The value of the BEGIN property should be \"vcard\", but it is \"" + value + "\".");
331                                    }
332                            } else if ("END".equals(typeName)) {
333                                    endFound = true;
334                                    if (!"vcard".equalsIgnoreCase(value)) {
335                                            warnings.add("The value of the END property should be \"vcard\", but it is \"" + value + "\".");
336                                    }
337                            } else if ("VERSION".equals(typeName)) {
338                                    if (version == null) {
339                                            version = VCardVersion.valueOfByStr(value);
340                                            if (version == null) {
341                                                    warnings.add("Invalid value of VERSION property: " + value);
342                                            }
343                                    } else {
344                                            warnings.add("Additional VERSION property encountered: \"" + value + "\".  It will be ignored.");
345                                    }
346                                    vcard.setVersion(version);
347                            } else {
348                                    //create the type object
349                                    VCardType type = createTypeObject(typeName);
350                                    type.setGroup(groupName);
351    
352                                    //unmarshal the text string into the object
353                                    warningsBuf.clear();
354                                    try {
355                                            type.unmarshalText(subTypes, value, version, warningsBuf, compatibilityMode);
356    
357                                            //add to vcard
358                                            if (type instanceof LabelType) {
359                                                    //LABELs must be treated specially so they can be matched up with their ADRs
360                                                    labels.add((LabelType) type);
361                                            } else {
362                                                    addToVCard(type, vcard);
363                                            }
364                                    } catch (SkipMeException e) {
365                                            warningsBuf.add(type.getTypeName() + " property will not be unmarshalled: " + e.getMessage());
366                                    } catch (EmbeddedVCardException e) {
367                                            //parse an embedded vCard (i.e. the AGENT type)
368    
369                                            VCardReader agentReader;
370                                            if (value.length() == 0 || version == null || version == VCardVersion.V2_1) {
371                                                    //vCard will be added as a nested vCard (2.1 style)
372                                                    agentReader = new VCardReader(reader);
373                                            } else {
374                                                    //vCard will be contained within the type value (3.0 style)
375                                                    value = VCardStringUtils.unescape(value);
376                                                    agentReader = new VCardReader(new StringReader(value));
377                                            }
378    
379                                            agentReader.setCompatibilityMode(compatibilityMode);
380                                            try {
381                                                    VCard agentVcard = agentReader.readNext();
382                                                    e.injectVCard(agentVcard);
383                                            } finally {
384                                                    for (String w : agentReader.getWarnings()) {
385                                                            warnings.add("Problem unmarshalling nested vCard value from " + type.getTypeName() + ": " + w);
386                                                    }
387                                            }
388    
389                                            addToVCard(type, vcard);
390                                    } finally {
391                                            warnings.addAll(warningsBuf);
392                                    }
393                            }
394                    }
395    
396                    if (typesRead == 0) {
397                            //end of stream reached
398                            return null;
399                    }
400    
401                    //assign labels to their addresses
402                    for (LabelType label : labels) {
403                            boolean orphaned = true;
404                            for (AddressType adr : vcard.getAddresses()) {
405                                    if (adr.getLabel() == null && adr.getTypes().equals(label.getTypes())) {
406                                            adr.setLabel(label.getValue());
407                                            orphaned = false;
408                                            break;
409                                    }
410                            }
411                            if (orphaned) {
412                                    vcard.addOrphanedLabel(label);
413                            }
414                    }
415    
416                    if (!endFound) {
417                            warnings.add("vCard does not terminate with the END property.");
418                    }
419    
420                    return vcard;
421            }
422    
423            /**
424             * Creates the appropriate {@link VCardType} instance, given the type name.
425             * This method does not unmarshal the type, it just creates the type object.
426             * @param name the type name (e.g. "FN")
427             * @return the Type that was created
428             */
429            private VCardType createTypeObject(String name) {
430                    Class<? extends VCardType> clazz = TypeList.getTypeClass(name);
431                    VCardType t;
432                    if (clazz != null) {
433                            try {
434                                    //create a new instance of the class
435                                    t = clazz.newInstance();
436                            } catch (Exception e) {
437                                    //it is the responsibility of the EZ-vCard developer to ensure that this exception is never thrown
438                                    //all type classes defined in the EZ-vCard library MUST have public, no-arg constructors
439                                    throw new RuntimeException(e);
440                            }
441                    } else {
442                            Class<? extends VCardType> extendedTypeClass = extendedTypeClasses.get(name);
443                            if (extendedTypeClass != null) {
444                                    try {
445                                            t = extendedTypeClass.newInstance();
446                                    } catch (Exception e) {
447                                            //this should never happen because the type class is checked to see if it has a public, no-arg constructor in the "registerExtendedType" method
448                                            throw new RuntimeException("Extended type class \"" + extendedTypeClass.getName() + "\" must have a public, no-arg constructor.");
449                                    }
450                            } else {
451                                    t = new RawType(name); //use RawType instead of TextType because we don't want to unescape any characters that might be meaningful to this type
452                                    if (!name.startsWith("X-")) {
453                                            warnings.add("Non-standard type \"" + name + "\" found.  Treating it as an extended type.");
454                                    }
455                            }
456                    }
457                    return t;
458            }
459    
460            /**
461             * Adds a type object to the vCard.
462             * @param t the type object
463             * @param vcard the vCard
464             */
465            private void addToVCard(VCardType t, VCard vcard) {
466                    Method method = TypeList.getAddMethod(t.getClass());
467                    if (method != null) {
468                            try {
469                                    method.invoke(vcard, t);
470                            } catch (Exception e) {
471                                    //this should NEVER be thrown because the method MUST be public
472                                    throw new RuntimeException(e);
473                            }
474                    } else {
475                            vcard.addExtendedType(t);
476                    }
477            }
478    
479            /**
480             * Gets the type name from a type class.
481             * @param clazz the type class
482             * @return the type name
483             */
484            private String getTypeNameFromTypeClass(Class<? extends VCardType> clazz) {
485                    try {
486                            VCardType t = clazz.newInstance();
487                            return t.getTypeName().toUpperCase();
488                    } catch (Exception e) {
489                            //there is no public, no-arg constructor
490                            throw new RuntimeException(e);
491                    }
492            }
493    
494            /**
495             * Closes the underlying {@link Reader} object.
496             */
497            public void close() throws IOException {
498                    reader.close();
499            }
500    }