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