VCardParserImpl_V30.java revision 4560bdde6dd75cca49fc55b58aafb5d416b88ca3
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 */
16package com.android.vcard;
17
18import android.util.Log;
19
20import com.android.vcard.exception.VCardException;
21
22import java.io.IOException;
23import 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 = "VCardParserImpl_V30";
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    @Override
154    protected void readEndVCard(boolean useCache, boolean allowGarbage)
155            throws IOException, VCardException {
156        // TODO: vCard 3.0 supports group.
157        super.readEndVCard(useCache, allowGarbage);
158    }
159
160    /**
161     * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not.
162     */
163    @Override
164    protected void handleParams(final String params) throws VCardException {
165        try {
166            super.handleParams(params);
167        } catch (VCardException e) {
168            // maybe IANA type
169            String[] strArray = params.split("=", 2);
170            if (strArray.length == 2) {
171                handleAnyParam(strArray[0], strArray[1]);
172            } else {
173                // Must not come here in the current implementation.
174                throw new VCardException(
175                        "Unknown params value: " + params);
176            }
177        }
178    }
179
180    @Override
181    protected void handleAnyParam(final String paramName, final String paramValue) {
182        mInterpreter.propertyParamType(paramName);
183        splitAndPutParamValue(paramValue);
184    }
185
186    @Override
187    protected void handleParamWithoutName(final String paramValue) {
188        handleType(paramValue);
189    }
190
191    /*
192     *  vCard 3.0 defines
193     *
194     *  param         = param-name "=" param-value *("," param-value)
195     *  param-name    = iana-token / x-name
196     *  param-value   = ptext / quoted-string
197     *  quoted-string = DQUOTE QSAFE-CHAR DQUOTE
198     *  QSAFE-CHAR    = WSP / %x21 / %x23-7E / NON-ASCII
199     *                ; Any character except CTLs, DQUOTE
200     *
201     *  QSAFE-CHAR must not contain DQUOTE, including escaped one (\").
202     */
203    @Override
204    protected void handleType(final String paramValue) {
205        mInterpreter.propertyParamType("TYPE");
206        splitAndPutParamValue(paramValue);
207    }
208
209    /**
210     * Splits parameter values into pieces in accordance with vCard 3.0 specification and
211     * puts pieces into mInterpreter.
212     */
213    /*
214     *  param-value   = ptext / quoted-string
215     *  quoted-string = DQUOTE QSAFE-CHAR DQUOTE
216     *  QSAFE-CHAR    = WSP / %x21 / %x23-7E / NON-ASCII
217     *                ; Any character except CTLs, DQUOTE
218     *
219     *  QSAFE-CHAR must not contain DQUOTE, including escaped one (\")
220     */
221    private void splitAndPutParamValue(String paramValue) {
222        // "comma,separated:inside.dquote",pref
223        //   -->
224        // - comma,separated:inside.dquote
225        // - pref
226        //
227        // Note: Though there's a code, we don't need to take much care of
228        // wrongly-added quotes like the example above, as they induce
229        // parse errors at the top level (when splitting a line into parts).
230        StringBuilder builder = null;  // Delay initialization.
231        boolean insideDquote = false;
232        final int length = paramValue.length();
233        for (int i = 0; i < length; i++) {
234            final char ch = paramValue.charAt(i);
235            if (ch == '"') {
236                if (insideDquote) {
237                    // End of Dquote.
238                    mInterpreter.propertyParamValue(builder.toString());
239                    builder = null;
240                    insideDquote = false;
241                } else {
242                    if (builder != null) {
243                        if (builder.length() > 0) {
244                            // e.g.
245                            // pref"quoted"
246                            Log.w(LOG_TAG, "Unexpected Dquote inside property.");
247                        } else {
248                            // e.g.
249                            // pref,"quoted"
250                            // "quoted",pref
251                            mInterpreter.propertyParamValue(builder.toString());
252                        }
253                    }
254                    insideDquote = true;
255                }
256            } else if (ch == ',' && !insideDquote) {
257                if (builder == null) {
258                    Log.w(LOG_TAG, "Comma is used before actual string comes. (" +
259                            paramValue + ")");
260                } else {
261                    mInterpreter.propertyParamValue(builder.toString());
262                    builder = null;
263                }
264            } else {
265                // To stop creating empty StringBuffer at the end of parameter,
266                // we delay creating this object until this point.
267                if (builder == null) {
268                    builder = new StringBuilder();
269                }
270                builder.append(ch);
271            }
272        }
273        if (insideDquote) {
274            // e.g.
275            // "non-quote-at-end
276            Log.d(LOG_TAG, "Dangling Dquote.");
277        }
278        if (builder != null) {
279            if (builder.length() == 0) {
280                Log.w(LOG_TAG, "Unintended behavior. We must not see empty StringBuilder " +
281                        "at the end of parameter value parsing.");
282            } else {
283                mInterpreter.propertyParamValue(builder.toString());
284            }
285        }
286    }
287
288    @Override
289    protected void handleAgent(final String propertyValue) {
290        // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.1.
291        //
292        // e.g.
293        // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n
294        //  TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n
295        //  ET:jfriday@host.com\nEND:VCARD\n
296        //
297        // TODO: fix this.
298        //
299        // issue:
300        //  vCard 3.0 also allows this as an example.
301        //
302        // AGENT;VALUE=uri:
303        //  CID:JQPUBLIC.part3.960129T083020.xyzMail@host3.com
304        //
305        // This is not vCard. Should we support this?
306        //
307        // Just ignore the line for now, since we cannot know how to handle it...
308        if (!mEmittedAgentWarning) {
309            Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it");
310            mEmittedAgentWarning = true;
311        }
312    }
313
314    /**
315     * vCard 3.0 does not require two CRLF at the last of BASE64 data.
316     * It only requires that data should be MIME-encoded.
317     */
318    @Override
319    protected String getBase64(final String firstString)
320            throws IOException, VCardException {
321        final StringBuilder builder = new StringBuilder();
322        builder.append(firstString);
323
324        while (true) {
325            final String line = getLine();
326            if (line == null) {
327                throw new VCardException("File ended during parsing BASE64 binary");
328            }
329            if (line.length() == 0) {
330                break;
331            } else if (!line.startsWith(" ") && !line.startsWith("\t")) {
332                mPreviousLine = line;
333                break;
334            }
335            builder.append(line);
336        }
337
338        return builder.toString();
339    }
340
341    /**
342     * ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N")
343     *              ; \\ encodes \, \n or \N encodes newline
344     *              ; \; encodes ;, \, encodes ,
345     *
346     * Note: Apple escapes ':' into '\:' while does not escape '\'
347     */
348    @Override
349    protected String maybeUnescapeText(final String text) {
350        return unescapeText(text);
351    }
352
353    public static String unescapeText(final String text) {
354        StringBuilder builder = new StringBuilder();
355        final int length = text.length();
356        for (int i = 0; i < length; i++) {
357            char ch = text.charAt(i);
358            if (ch == '\\' && i < length - 1) {
359                final char next_ch = text.charAt(++i);
360                if (next_ch == 'n' || next_ch == 'N') {
361                    builder.append("\n");
362                } else {
363                    builder.append(next_ch);
364                }
365            } else {
366                builder.append(ch);
367            }
368        }
369        return builder.toString();
370    }
371
372    @Override
373    protected String maybeUnescapeCharacter(final char ch) {
374        return unescapeCharacter(ch);
375    }
376
377    public static String unescapeCharacter(final char ch) {
378        if (ch == 'n' || ch == 'N') {
379            return "\n";
380        } else {
381            return String.valueOf(ch);
382        }
383    }
384
385    @Override
386    protected Set<String> getKnownPropertyNameSet() {
387        return VCardParser_V30.sKnownPropertyNameSet;
388    }
389}
390