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 
VCardParserImpl_V30()44     public VCardParserImpl_V30() {
45         super();
46     }
47 
VCardParserImpl_V30(int vcardType)48     public VCardParserImpl_V30(int vcardType) {
49         super(vcardType);
50     }
51 
52     @Override
getVersion()53     protected int getVersion() {
54         return VCardConfig.VERSION_30;
55     }
56 
57     @Override
getVersionString()58     protected String getVersionString() {
59         return VCardConstants.VERSION_V30;
60     }
61 
62     @Override
getLine()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
getNonEmptyLine()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
readBeginVCard(boolean allowGarbage)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
handleParams(VCardProperty propertyData, final String params)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
handleAnyParam( VCardProperty propertyData, final String paramName, final String paramValue)152     protected void handleAnyParam(
153             VCardProperty propertyData, final String paramName, final String paramValue) {
154         splitAndPutParam(propertyData, paramName, paramValue);
155     }
156 
157     @Override
handleParamWithoutName(VCardProperty property, final String paramValue)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
handleType(VCardProperty property, final String paramValue)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      */
splitAndPutParam(VCardProperty property, String paramName, String paramValue)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      */
encodeParamValue(String paramValue)261     protected String encodeParamValue(String paramValue) {
262         return VCardUtils.convertStringCharset(
263                 paramValue, VCardConfig.DEFAULT_INTERMEDIATE_CHARSET, "UTF-8");
264     }
265 
266     @Override
handleAgent(VCardProperty property)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@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@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
getBase64(final String firstString)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
maybeUnescapeText(final String text)313     protected String maybeUnescapeText(final String text) {
314         return unescapeText(text);
315     }
316 
unescapeText(final String text)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
maybeUnescapeCharacter(final char ch)337     protected String maybeUnescapeCharacter(final char ch) {
338         return unescapeCharacter(ch);
339     }
340 
unescapeCharacter(final char ch)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
getKnownPropertyNameSet()350     protected Set<String> getKnownPropertyNameSet() {
351         return VCardParser_V30.sKnownPropertyNameSet;
352     }
353 }
354