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 peekLine() throws IOException {
     64         if (mPreviousLine != null) {
     65             String ret = mPreviousLine;
     66             return ret;
     67         } else {
     68             return mReader.peekLine();
     69         }
     70     }
     71 
     72     @Override
     73     protected String getLine() throws IOException {
     74         if (mPreviousLine != null) {
     75             String ret = mPreviousLine;
     76             mPreviousLine = null;
     77             return ret;
     78         } else {
     79             return mReader.readLine();
     80         }
     81     }
     82 
     83     /**
     84      * vCard 3.0 requires that the line with space at the beginning of the line
     85      * must be combined with previous line.
     86      */
     87     @Override
     88     protected String getNonEmptyLine() throws IOException, VCardException {
     89         String line;
     90         StringBuilder builder = null;
     91         while ((line = mReader.readLine()) != null) {
     92             // Skip empty lines in order to accomodate implementations that
     93             // send line termination variations such as \r\r\n.
     94             if (line.length() == 0) {
     95                 continue;
     96             } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') {
     97                 // RFC 2425 describes line continuation as \r\n followed by
     98                 // a single ' ' or '\t' whitespace character.
     99                 if (builder == null) {
    100                     builder = new StringBuilder();
    101                 }
    102                 if (mPreviousLine != null) {
    103                     builder.append(mPreviousLine);
    104                     mPreviousLine = null;
    105                 }
    106                 builder.append(line.substring(1));
    107             } else {
    108                 if (builder != null || mPreviousLine != null) {
    109                     break;
    110                 }
    111                 mPreviousLine = line;
    112             }
    113         }
    114 
    115         String ret = null;
    116         if (builder != null) {
    117             ret = builder.toString();
    118         } else if (mPreviousLine != null) {
    119             ret = mPreviousLine;
    120         }
    121         mPreviousLine = line;
    122         if (ret == null) {
    123             throw new VCardException("Reached end of buffer.");
    124         }
    125         return ret;
    126     }
    127 
    128     /*
    129      * vcard = [group "."] "BEGIN" ":" "VCARD" 1 * CRLF
    130      *         1 * (contentline)
    131      *         ;A vCard object MUST include the VERSION, FN and N types.
    132      *         [group "."] "END" ":" "VCARD" 1 * CRLF
    133      */
    134     @Override
    135     protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException {
    136         // TODO: vCard 3.0 supports group.
    137         return super.readBeginVCard(allowGarbage);
    138     }
    139 
    140     /**
    141      * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not.
    142      */
    143     @Override
    144     protected void handleParams(VCardProperty propertyData, final String params)
    145             throws VCardException {
    146         try {
    147             super.handleParams(propertyData, params);
    148         } catch (VCardException e) {
    149             // maybe IANA type
    150             String[] strArray = params.split("=", 2);
    151             if (strArray.length == 2) {
    152                 handleAnyParam(propertyData, strArray[0], strArray[1]);
    153             } else {
    154                 // Must not come here in the current implementation.
    155                 throw new VCardException(
    156                         "Unknown params value: " + params);
    157             }
    158         }
    159     }
    160 
    161     @Override
    162     protected void handleAnyParam(
    163             VCardProperty propertyData, final String paramName, final String paramValue) {
    164         splitAndPutParam(propertyData, paramName, paramValue);
    165     }
    166 
    167     @Override
    168     protected void handleParamWithoutName(VCardProperty property, final String paramValue) {
    169         handleType(property, paramValue);
    170     }
    171 
    172     /*
    173      *  vCard 3.0 defines
    174      *
    175      *  param         = param-name "=" param-value *("," param-value)
    176      *  param-name    = iana-token / x-name
    177      *  param-value   = ptext / quoted-string
    178      *  quoted-string = DQUOTE QSAFE-CHAR DQUOTE
    179      *  QSAFE-CHAR    = WSP / %x21 / %x23-7E / NON-ASCII
    180      *                ; Any character except CTLs, DQUOTE
    181      *
    182      *  QSAFE-CHAR must not contain DQUOTE, including escaped one (\").
    183      */
    184     @Override
    185     protected void handleType(VCardProperty property, final String paramValue) {
    186         splitAndPutParam(property, VCardConstants.PARAM_TYPE, paramValue);
    187     }
    188 
    189     /**
    190      * Splits parameter values into pieces in accordance with vCard 3.0 specification and
    191      * puts pieces into mInterpreter.
    192      */
    193     /*
    194      *  param-value   = ptext / quoted-string
    195      *  quoted-string = DQUOTE QSAFE-CHAR DQUOTE
    196      *  QSAFE-CHAR    = WSP / %x21 / %x23-7E / NON-ASCII
    197      *                ; Any character except CTLs, DQUOTE
    198      *
    199      *  QSAFE-CHAR must not contain DQUOTE, including escaped one (\")
    200      */
    201     private void splitAndPutParam(VCardProperty property, String paramName, String paramValue) {
    202         // "comma,separated:inside.dquote",pref
    203         //   -->
    204         // - comma,separated:inside.dquote
    205         // - pref
    206         //
    207         // Note: Though there's a code, we don't need to take much care of
    208         // wrongly-added quotes like the example above, as they induce
    209         // parse errors at the top level (when splitting a line into parts).
    210         StringBuilder builder = null;  // Delay initialization.
    211         boolean insideDquote = false;
    212         final int length = paramValue.length();
    213         for (int i = 0; i < length; i++) {
    214             final char ch = paramValue.charAt(i);
    215             if (ch == '"') {
    216                 if (insideDquote) {
    217                     // End of Dquote.
    218                     property.addParameter(paramName, encodeParamValue(builder.toString()));
    219                     builder = null;
    220                     insideDquote = false;
    221                 } else {
    222                     if (builder != null) {
    223                         if (builder.length() > 0) {
    224                             // e.g.
    225                             // pref"quoted"
    226                             Log.w(LOG_TAG, "Unexpected Dquote inside property.");
    227                         } else {
    228                             // e.g.
    229                             // pref,"quoted"
    230                             // "quoted",pref
    231                             property.addParameter(paramName, encodeParamValue(builder.toString()));
    232                         }
    233                     }
    234                     insideDquote = true;
    235                 }
    236             } else if (ch == ',' && !insideDquote) {
    237                 if (builder == null) {
    238                     Log.w(LOG_TAG, "Comma is used before actual string comes. (" +
    239                             paramValue + ")");
    240                 } else {
    241                     property.addParameter(paramName, encodeParamValue(builder.toString()));
    242                     builder = null;
    243                 }
    244             } else {
    245                 // To stop creating empty StringBuffer at the end of parameter,
    246                 // we delay creating this object until this point.
    247                 if (builder == null) {
    248                     builder = new StringBuilder();
    249                 }
    250                 builder.append(ch);
    251             }
    252         }
    253         if (insideDquote) {
    254             // e.g.
    255             // "non-quote-at-end
    256             Log.d(LOG_TAG, "Dangling Dquote.");
    257         }
    258         if (builder != null) {
    259             if (builder.length() == 0) {
    260                 Log.w(LOG_TAG, "Unintended behavior. We must not see empty StringBuilder " +
    261                         "at the end of parameter value parsing.");
    262             } else {
    263                 property.addParameter(paramName, encodeParamValue(builder.toString()));
    264             }
    265         }
    266     }
    267 
    268     /**
    269      * Encode a param value using UTF-8.
    270      */
    271     protected String encodeParamValue(String paramValue) {
    272         return VCardUtils.convertStringCharset(
    273                 paramValue, VCardConfig.DEFAULT_INTERMEDIATE_CHARSET, "UTF-8");
    274     }
    275 
    276     @Override
    277     protected void handleAgent(VCardProperty property) {
    278         // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.1.
    279         //
    280         // e.g.
    281         // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n
    282         //  TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n
    283         //  ET:jfriday (at) host.com\nEND:VCARD\n
    284         //
    285         // TODO: fix this.
    286         //
    287         // issue:
    288         //  vCard 3.0 also allows this as an example.
    289         //
    290         // AGENT;VALUE=uri:
    291         //  CID:JQPUBLIC.part3.960129T083020.xyzMail (at) host3.com
    292         //
    293         // This is not vCard. Should we support this?
    294         //
    295         // Just ignore the line for now, since we cannot know how to handle it...
    296         if (!mEmittedAgentWarning) {
    297             Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it");
    298             mEmittedAgentWarning = true;
    299         }
    300     }
    301 
    302     /**
    303      * This is only called from handlePropertyValue(), which has already
    304      * read the first line of this property. With v3.0, the getNonEmptyLine()
    305      * routine has already concatenated all following continuation lines.
    306      * The routine is implemented in the V21 parser to concatenate v2.1 style
    307      * data blocks, but is unnecessary here.
    308      */
    309     @Override
    310     protected String getBase64(final String firstString)
    311             throws IOException, VCardException {
    312         return firstString;
    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(final String text) {
    324         return unescapeText(text);
    325     }
    326 
    327     public static String unescapeText(final String text) {
    328         StringBuilder builder = new StringBuilder();
    329         final 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                 final 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(final char ch) {
    348         return unescapeCharacter(ch);
    349     }
    350 
    351     public static String unescapeCharacter(final char ch) {
    352         if (ch == 'n' || ch == 'N') {
    353             return "\n";
    354         } else {
    355             return String.valueOf(ch);
    356         }
    357     }
    358 
    359     @Override
    360     protected Set<String> getKnownPropertyNameSet() {
    361         return VCardParser_V30.sKnownPropertyNameSet;
    362     }
    363 }
    364