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