Home | History | Annotate | Download | only in vcard
      1 /*
      2  * Copyright (C) 2010 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 com.android.vcard;
     17 
     18 import android.util.Log;
     19 
     20 import com.android.vcard.exception.VCardException;
     21 
     22 import java.io.IOException;
     23 import java.util.Set;
     24 
     25 /**
     26  * <p>
     27  * Basic implementation achieving vCard 3.0 parsing.
     28  * </p>
     29  * <p>
     30  * This class inherits vCard 2.1 implementation since technically they are similar,
     31  * while specifically there's logical no relevance between them.
     32  * So that developers are not confused with the inheritance,
     33  * {@link VCardParser_V30} does not inherit {@link VCardParser_V21}, while
     34  * {@link VCardParserImpl_V30} inherits {@link VCardParserImpl_V21}.
     35  * </p>
     36  * @hide
     37  */
     38 /* package */ class VCardParserImpl_V30 extends VCardParserImpl_V21 {
     39     private static final String LOG_TAG = VCardConstants.LOG_TAG;
     40 
     41     private String mPreviousLine;
     42     private boolean mEmittedAgentWarning = false;
     43 
     44     public VCardParserImpl_V30() {
     45         super();
     46     }
     47 
     48     public VCardParserImpl_V30(int vcardType) {
     49         super(vcardType);
     50     }
     51 
     52     @Override
     53     protected int getVersion() {
     54         return VCardConfig.VERSION_30;
     55     }
     56 
     57     @Override
     58     protected String getVersionString() {
     59         return VCardConstants.VERSION_V30;
     60     }
     61 
     62     @Override
     63     protected String getLine() throws IOException {
     64         if (mPreviousLine != null) {
     65             String ret = mPreviousLine;
     66             mPreviousLine = null;
     67             return ret;
     68         } else {
     69             return mReader.readLine();
     70         }
     71     }
     72 
     73     /**
     74      * vCard 3.0 requires that the line with space at the beginning of the line
     75      * must be combined with previous line.
     76      */
     77     @Override
     78     protected String getNonEmptyLine() throws IOException, VCardException {
     79         String line;
     80         StringBuilder builder = null;
     81         while (true) {
     82             line = mReader.readLine();
     83             if (line == null) {
     84                 if (builder != null) {
     85                     return builder.toString();
     86                 } else if (mPreviousLine != null) {
     87                     String ret = mPreviousLine;
     88                     mPreviousLine = null;
     89                     return ret;
     90                 }
     91                 throw new VCardException("Reached end of buffer.");
     92             } else if (line.length() == 0) {
     93                 if (builder != null) {
     94                     return builder.toString();
     95                 } else if (mPreviousLine != null) {
     96                     String ret = mPreviousLine;
     97                     mPreviousLine = null;
     98                     return ret;
     99                 }
    100             } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') {
    101                 if (builder != null) {
    102                     // See Section 5.8.1 of RFC 2425 (MIME-DIR document).
    103                     // Following is the excerpts from it.
    104                     //
    105                     // DESCRIPTION:This is a long description that exists on a long line.
    106                     //
    107                     // Can be represented as:
    108                     //
    109                     // DESCRIPTION:This is a long description
    110                     //  that exists on a long line.
    111                     //
    112                     // It could also be represented as:
    113                     //
    114                     // DESCRIPTION:This is a long descrip
    115                     //  tion that exists o
    116                     //  n a long line.
    117                     builder.append(line.substring(1));
    118                 } else if (mPreviousLine != null) {
    119                     builder = new StringBuilder();
    120                     builder.append(mPreviousLine);
    121                     mPreviousLine = null;
    122                     builder.append(line.substring(1));
    123                 } else {
    124                     throw new VCardException("Space exists at the beginning of the line");
    125                 }
    126             } else {
    127                 if (mPreviousLine == null) {
    128                     mPreviousLine = line;
    129                     if (builder != null) {
    130                         return builder.toString();
    131                     }
    132                 } else {
    133                     String ret = mPreviousLine;
    134                     mPreviousLine = line;
    135                     return ret;
    136                 }
    137             }
    138         }
    139     }
    140 
    141     /*
    142      * vcard = [group "."] "BEGIN" ":" "VCARD" 1 * CRLF
    143      *         1 * (contentline)
    144      *         ;A vCard object MUST include the VERSION, FN and N types.
    145      *         [group "."] "END" ":" "VCARD" 1 * CRLF
    146      */
    147     @Override
    148     protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException {
    149         // TODO: vCard 3.0 supports group.
    150         return super.readBeginVCard(allowGarbage);
    151     }
    152 
    153     /**
    154      * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not.
    155      */
    156     @Override
    157     protected void handleParams(VCardProperty propertyData, final String params)
    158             throws VCardException {
    159         try {
    160             super.handleParams(propertyData, params);
    161         } catch (VCardException e) {
    162             // maybe IANA type
    163             String[] strArray = params.split("=", 2);
    164             if (strArray.length == 2) {
    165                 handleAnyParam(propertyData, strArray[0], strArray[1]);
    166             } else {
    167                 // Must not come here in the current implementation.
    168                 throw new VCardException(
    169                         "Unknown params value: " + params);
    170             }
    171         }
    172     }
    173 
    174     @Override
    175     protected void handleAnyParam(
    176             VCardProperty propertyData, final String paramName, final String paramValue) {
    177         splitAndPutParam(propertyData, paramName, paramValue);
    178     }
    179 
    180     @Override
    181     protected void handleParamWithoutName(VCardProperty property, final String paramValue) {
    182         handleType(property, paramValue);
    183     }
    184 
    185     /*
    186      *  vCard 3.0 defines
    187      *
    188      *  param         = param-name "=" param-value *("," param-value)
    189      *  param-name    = iana-token / x-name
    190      *  param-value   = ptext / quoted-string
    191      *  quoted-string = DQUOTE QSAFE-CHAR DQUOTE
    192      *  QSAFE-CHAR    = WSP / %x21 / %x23-7E / NON-ASCII
    193      *                ; Any character except CTLs, DQUOTE
    194      *
    195      *  QSAFE-CHAR must not contain DQUOTE, including escaped one (\").
    196      */
    197     @Override
    198     protected void handleType(VCardProperty property, final String paramValue) {
    199         splitAndPutParam(property, VCardConstants.PARAM_TYPE, paramValue);
    200     }
    201 
    202     /**
    203      * Splits parameter values into pieces in accordance with vCard 3.0 specification and
    204      * puts pieces into mInterpreter.
    205      */
    206     /*
    207      *  param-value   = ptext / quoted-string
    208      *  quoted-string = DQUOTE QSAFE-CHAR DQUOTE
    209      *  QSAFE-CHAR    = WSP / %x21 / %x23-7E / NON-ASCII
    210      *                ; Any character except CTLs, DQUOTE
    211      *
    212      *  QSAFE-CHAR must not contain DQUOTE, including escaped one (\")
    213      */
    214     private void splitAndPutParam(VCardProperty property, String paramName, String paramValue) {
    215         // "comma,separated:inside.dquote",pref
    216         //   -->
    217         // - comma,separated:inside.dquote
    218         // - pref
    219         //
    220         // Note: Though there's a code, we don't need to take much care of
    221         // wrongly-added quotes like the example above, as they induce
    222         // parse errors at the top level (when splitting a line into parts).
    223         StringBuilder builder = null;  // Delay initialization.
    224         boolean insideDquote = false;
    225         final int length = paramValue.length();
    226         for (int i = 0; i < length; i++) {
    227             final char ch = paramValue.charAt(i);
    228             if (ch == '"') {
    229                 if (insideDquote) {
    230                     // End of Dquote.
    231                     property.addParameter(paramName, encodeParamValue(builder.toString()));
    232                     builder = null;
    233                     insideDquote = false;
    234                 } else {
    235                     if (builder != null) {
    236                         if (builder.length() > 0) {
    237                             // e.g.
    238                             // pref"quoted"
    239                             Log.w(LOG_TAG, "Unexpected Dquote inside property.");
    240                         } else {
    241                             // e.g.
    242                             // pref,"quoted"
    243                             // "quoted",pref
    244                             property.addParameter(paramName, encodeParamValue(builder.toString()));
    245                         }
    246                     }
    247                     insideDquote = true;
    248                 }
    249             } else if (ch == ',' && !insideDquote) {
    250                 if (builder == null) {
    251                     Log.w(LOG_TAG, "Comma is used before actual string comes. (" +
    252                             paramValue + ")");
    253                 } else {
    254                     property.addParameter(paramName, encodeParamValue(builder.toString()));
    255                     builder = null;
    256                 }
    257             } else {
    258                 // To stop creating empty StringBuffer at the end of parameter,
    259                 // we delay creating this object until this point.
    260                 if (builder == null) {
    261                     builder = new StringBuilder();
    262                 }
    263                 builder.append(ch);
    264             }
    265         }
    266         if (insideDquote) {
    267             // e.g.
    268             // "non-quote-at-end
    269             Log.d(LOG_TAG, "Dangling Dquote.");
    270         }
    271         if (builder != null) {
    272             if (builder.length() == 0) {
    273                 Log.w(LOG_TAG, "Unintended behavior. We must not see empty StringBuilder " +
    274                         "at the end of parameter value parsing.");
    275             } else {
    276                 property.addParameter(paramName, encodeParamValue(builder.toString()));
    277             }
    278         }
    279     }
    280 
    281     /**
    282      * Encode a param value using UTF-8.
    283      */
    284     protected String encodeParamValue(String paramValue) {
    285         return VCardUtils.convertStringCharset(
    286                 paramValue, VCardConfig.DEFAULT_INTERMEDIATE_CHARSET, "UTF-8");
    287     }
    288 
    289     @Override
    290     protected void handleAgent(VCardProperty property) {
    291         // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.1.
    292         //
    293         // e.g.
    294         // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n
    295         //  TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n
    296         //  ET:jfriday (at) host.com\nEND:VCARD\n
    297         //
    298         // TODO: fix this.
    299         //
    300         // issue:
    301         //  vCard 3.0 also allows this as an example.
    302         //
    303         // AGENT;VALUE=uri:
    304         //  CID:JQPUBLIC.part3.960129T083020.xyzMail (at) host3.com
    305         //
    306         // This is not vCard. Should we support this?
    307         //
    308         // Just ignore the line for now, since we cannot know how to handle it...
    309         if (!mEmittedAgentWarning) {
    310             Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it");
    311             mEmittedAgentWarning = true;
    312         }
    313     }
    314 
    315     /**
    316      * vCard 3.0 does not require two CRLF at the last of BASE64 data.
    317      * It only requires that data should be MIME-encoded.
    318      */
    319     @Override
    320     protected String getBase64(final String firstString)
    321             throws IOException, VCardException {
    322         final StringBuilder builder = new StringBuilder();
    323         builder.append(firstString);
    324 
    325         while (true) {
    326             final String line = getLine();
    327             if (line == null) {
    328                 throw new VCardException("File ended during parsing BASE64 binary");
    329             }
    330             if (line.length() == 0) {
    331                 break;
    332             } else if (!line.startsWith(" ") && !line.startsWith("\t")) {
    333                 mPreviousLine = line;
    334                 break;
    335             }
    336             builder.append(line);
    337         }
    338 
    339         return builder.toString();
    340     }
    341 
    342     /**
    343      * ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N")
    344      *              ; \\ encodes \, \n or \N encodes newline
    345      *              ; \; encodes ;, \, encodes ,
    346      *
    347      * Note: Apple escapes ':' into '\:' while does not escape '\'
    348      */
    349     @Override
    350     protected String maybeUnescapeText(final String text) {
    351         return unescapeText(text);
    352     }
    353 
    354     public static String unescapeText(final String text) {
    355         StringBuilder builder = new StringBuilder();
    356         final int length = text.length();
    357         for (int i = 0; i < length; i++) {
    358             char ch = text.charAt(i);
    359             if (ch == '\\' && i < length - 1) {
    360                 final char next_ch = text.charAt(++i);
    361                 if (next_ch == 'n' || next_ch == 'N') {
    362                     builder.append("\n");
    363                 } else {
    364                     builder.append(next_ch);
    365                 }
    366             } else {
    367                 builder.append(ch);
    368             }
    369         }
    370         return builder.toString();
    371     }
    372 
    373     @Override
    374     protected String maybeUnescapeCharacter(final char ch) {
    375         return unescapeCharacter(ch);
    376     }
    377 
    378     public static String unescapeCharacter(final char ch) {
    379         if (ch == 'n' || ch == 'N') {
    380             return "\n";
    381         } else {
    382             return String.valueOf(ch);
    383         }
    384     }
    385 
    386     @Override
    387     protected Set<String> getKnownPropertyNameSet() {
    388         return VCardParser_V30.sKnownPropertyNameSet;
    389     }
    390 }
    391