Home | History | Annotate | Download | only in vcard
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package android.pim.vcard;
     17 
     18 import android.pim.vcard.exception.VCardException;
     19 import android.util.Log;
     20 
     21 import java.io.IOException;
     22 import java.util.Arrays;
     23 import java.util.HashSet;
     24 
     25 /**
     26  * The class used to parse vCard 3.0.
     27  * Please refer to vCard Specification 3.0 (http://tools.ietf.org/html/rfc2426).
     28  */
     29 public class VCardParser_V30 extends VCardParser_V21 {
     30     private static final String LOG_TAG = "VCardParser_V30";
     31 
     32     private static final HashSet<String> sAcceptablePropsWithParam = new HashSet<String>(
     33             Arrays.asList(
     34                     "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND",
     35                     "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL",
     36                     "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER", // 2.1
     37                     "NAME", "PROFILE", "SOURCE", "NICKNAME", "CLASS",
     38                     "SORT-STRING", "CATEGORIES", "PRODID")); // 3.0
     39 
     40     // Although "7bit" and "BASE64" is not allowed in vCard 3.0, we allow it for safety.
     41     private static final HashSet<String> sAcceptableEncodingV30 = new HashSet<String>(
     42             Arrays.asList("7BIT", "8BIT", "BASE64", "B"));
     43 
     44     // Although RFC 2426 specifies some property must not have parameters, we allow it,
     45     // since there may be some careers which violates the RFC...
     46     private static final HashSet<String> acceptablePropsWithoutParam = new HashSet<String>();
     47 
     48     private String mPreviousLine;
     49 
     50     private boolean mEmittedAgentWarning = false;
     51 
     52     /**
     53      * True when the caller wants the parser to be strict about the input.
     54      * Currently this is only for testing.
     55      */
     56     private final boolean mStrictParsing;
     57 
     58     public VCardParser_V30() {
     59         super();
     60         mStrictParsing = false;
     61     }
     62 
     63     /**
     64      * @param strictParsing when true, this object throws VCardException when the vcard is not
     65      * valid from the view of vCard 3.0 specification (defined in RFC 2426). Note that this class
     66      * is not fully yet for being used with this flag and may not notice invalid line(s).
     67      *
     68      * @hide currently only for testing!
     69      */
     70     public VCardParser_V30(boolean strictParsing) {
     71         super();
     72         mStrictParsing = strictParsing;
     73     }
     74 
     75     public VCardParser_V30(int parseMode) {
     76         super(parseMode);
     77         mStrictParsing = false;
     78     }
     79 
     80     @Override
     81     protected int getVersion() {
     82         return VCardConfig.FLAG_V30;
     83     }
     84 
     85     @Override
     86     protected String getVersionString() {
     87         return VCardConstants.VERSION_V30;
     88     }
     89 
     90     @Override
     91     protected boolean isValidPropertyName(String propertyName) {
     92         if (!(sAcceptablePropsWithParam.contains(propertyName) ||
     93                 acceptablePropsWithoutParam.contains(propertyName) ||
     94                 propertyName.startsWith("X-")) &&
     95                 !mUnknownTypeMap.contains(propertyName)) {
     96             mUnknownTypeMap.add(propertyName);
     97             Log.w(LOG_TAG, "Property name unsupported by vCard 3.0: " + propertyName);
     98         }
     99         return true;
    100     }
    101 
    102     @Override
    103     protected boolean isValidEncoding(String encoding) {
    104         return sAcceptableEncodingV30.contains(encoding.toUpperCase());
    105     }
    106 
    107     @Override
    108     protected String getLine() throws IOException {
    109         if (mPreviousLine != null) {
    110             String ret = mPreviousLine;
    111             mPreviousLine = null;
    112             return ret;
    113         } else {
    114             return mReader.readLine();
    115         }
    116     }
    117 
    118     /**
    119      * vCard 3.0 requires that the line with space at the beginning of the line
    120      * must be combined with previous line.
    121      */
    122     @Override
    123     protected String getNonEmptyLine() throws IOException, VCardException {
    124         String line;
    125         StringBuilder builder = null;
    126         while (true) {
    127             line = mReader.readLine();
    128             if (line == null) {
    129                 if (builder != null) {
    130                     return builder.toString();
    131                 } else if (mPreviousLine != null) {
    132                     String ret = mPreviousLine;
    133                     mPreviousLine = null;
    134                     return ret;
    135                 }
    136                 throw new VCardException("Reached end of buffer.");
    137             } else if (line.length() == 0) {
    138                 if (builder != null) {
    139                     return builder.toString();
    140                 } else if (mPreviousLine != null) {
    141                     String ret = mPreviousLine;
    142                     mPreviousLine = null;
    143                     return ret;
    144                 }
    145             } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') {
    146                 if (builder != null) {
    147                     // See Section 5.8.1 of RFC 2425 (MIME-DIR document).
    148                     // Following is the excerpts from it.
    149                     //
    150                     // DESCRIPTION:This is a long description that exists on a long line.
    151                     //
    152                     // Can be represented as:
    153                     //
    154                     // DESCRIPTION:This is a long description
    155                     //  that exists on a long line.
    156                     //
    157                     // It could also be represented as:
    158                     //
    159                     // DESCRIPTION:This is a long descrip
    160                     //  tion that exists o
    161                     //  n a long line.
    162                     builder.append(line.substring(1));
    163                 } else if (mPreviousLine != null) {
    164                     builder = new StringBuilder();
    165                     builder.append(mPreviousLine);
    166                     mPreviousLine = null;
    167                     builder.append(line.substring(1));
    168                 } else {
    169                     throw new VCardException("Space exists at the beginning of the line");
    170                 }
    171             } else {
    172                 if (mPreviousLine == null) {
    173                     mPreviousLine = line;
    174                     if (builder != null) {
    175                         return builder.toString();
    176                     }
    177                 } else {
    178                     String ret = mPreviousLine;
    179                     mPreviousLine = line;
    180                     return ret;
    181                 }
    182             }
    183         }
    184     }
    185 
    186 
    187     /**
    188      * vcard = [group "."] "BEGIN" ":" "VCARD" 1 * CRLF
    189      *         1 * (contentline)
    190      *         ;A vCard object MUST include the VERSION, FN and N types.
    191      *         [group "."] "END" ":" "VCARD" 1 * CRLF
    192      */
    193     @Override
    194     protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException {
    195         // TODO: vCard 3.0 supports group.
    196         return super.readBeginVCard(allowGarbage);
    197     }
    198 
    199     @Override
    200     protected void readEndVCard(boolean useCache, boolean allowGarbage)
    201             throws IOException, VCardException {
    202         // TODO: vCard 3.0 supports group.
    203         super.readEndVCard(useCache, allowGarbage);
    204     }
    205 
    206     /**
    207      * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not.
    208      */
    209     @Override
    210     protected void handleParams(String params) throws VCardException {
    211         try {
    212             super.handleParams(params);
    213         } catch (VCardException e) {
    214             // maybe IANA type
    215             String[] strArray = params.split("=", 2);
    216             if (strArray.length == 2) {
    217                 handleAnyParam(strArray[0], strArray[1]);
    218             } else {
    219                 // Must not come here in the current implementation.
    220                 throw new VCardException(
    221                         "Unknown params value: " + params);
    222             }
    223         }
    224     }
    225 
    226     @Override
    227     protected void handleAnyParam(String paramName, String paramValue) {
    228         super.handleAnyParam(paramName, paramValue);
    229     }
    230 
    231     @Override
    232     protected void handleParamWithoutName(final String paramValue) throws VCardException {
    233         if (mStrictParsing) {
    234             throw new VCardException("Parameter without name is not acceptable in vCard 3.0");
    235         } else {
    236             super.handleParamWithoutName(paramValue);
    237         }
    238     }
    239 
    240     /**
    241      *  vCard 3.0 defines
    242      *
    243      *  param         = param-name "=" param-value *("," param-value)
    244      *  param-name    = iana-token / x-name
    245      *  param-value   = ptext / quoted-string
    246      *  quoted-string = DQUOTE QSAFE-CHAR DQUOTE
    247      */
    248     @Override
    249     protected void handleType(String ptypevalues) {
    250         String[] ptypeArray = ptypevalues.split(",");
    251         mBuilder.propertyParamType("TYPE");
    252         for (String value : ptypeArray) {
    253             int length = value.length();
    254             if (length >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
    255                 mBuilder.propertyParamValue(value.substring(1, value.length() - 1));
    256             } else {
    257                 mBuilder.propertyParamValue(value);
    258             }
    259         }
    260     }
    261 
    262     @Override
    263     protected void handleAgent(String propertyValue) {
    264         // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.1.
    265         //
    266         // e.g.
    267         // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n
    268         //  TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n
    269         //  ET:jfriday (at) host.com\nEND:VCARD\n
    270         //
    271         // TODO: fix this.
    272         //
    273         // issue:
    274         //  vCard 3.0 also allows this as an example.
    275         //
    276         // AGENT;VALUE=uri:
    277         //  CID:JQPUBLIC.part3.960129T083020.xyzMail (at) host3.com
    278         //
    279         // This is not vCard. Should we support this?
    280         //
    281         // Just ignore the line for now, since we cannot know how to handle it...
    282         if (!mEmittedAgentWarning) {
    283             Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it");
    284             mEmittedAgentWarning = true;
    285         }
    286     }
    287 
    288     /**
    289      * vCard 3.0 does not require two CRLF at the last of BASE64 data.
    290      * It only requires that data should be MIME-encoded.
    291      */
    292     @Override
    293     protected String getBase64(String firstString) throws IOException, VCardException {
    294         StringBuilder builder = new StringBuilder();
    295         builder.append(firstString);
    296 
    297         while (true) {
    298             String line = getLine();
    299             if (line == null) {
    300                 throw new VCardException(
    301                         "File ended during parsing BASE64 binary");
    302             }
    303             if (line.length() == 0) {
    304                 break;
    305             } else if (!line.startsWith(" ") && !line.startsWith("\t")) {
    306                 mPreviousLine = line;
    307                 break;
    308             }
    309             builder.append(line);
    310         }
    311 
    312         return builder.toString();
    313     }
    314 
    315     /**
    316      * ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N")
    317      *              ; \\ encodes \, \n or \N encodes newline
    318      *              ; \; encodes ;, \, encodes ,
    319      *
    320      * Note: Apple escapes ':' into '\:' while does not escape '\'
    321      */
    322     @Override
    323     protected String maybeUnescapeText(String text) {
    324         return unescapeText(text);
    325     }
    326 
    327     public static String unescapeText(String text) {
    328         StringBuilder builder = new StringBuilder();
    329         int length = text.length();
    330         for (int i = 0; i < length; i++) {
    331             char ch = text.charAt(i);
    332             if (ch == '\\' && i < length - 1) {
    333                 char next_ch = text.charAt(++i);
    334                 if (next_ch == 'n' || next_ch == 'N') {
    335                     builder.append("\n");
    336                 } else {
    337                     builder.append(next_ch);
    338                 }
    339             } else {
    340                 builder.append(ch);
    341             }
    342         }
    343         return builder.toString();
    344     }
    345 
    346     @Override
    347     protected String maybeUnescapeCharacter(char ch) {
    348         return unescapeCharacter(ch);
    349     }
    350 
    351     public static String unescapeCharacter(char ch) {
    352         if (ch == 'n' || ch == 'N') {
    353             return "\n";
    354         } else {
    355             return String.valueOf(ch);
    356         }
    357     }
    358 }
    359