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 = 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@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
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