1/*
2 * Copyright (C) 2014 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
17package android.bluetooth.client.map;
18
19import android.util.Log;
20
21import com.android.vcard.VCardEntry;
22import com.android.vcard.VCardEntryConstructor;
23import com.android.vcard.VCardEntryHandler;
24import com.android.vcard.VCardParser;
25import com.android.vcard.VCardParser_V21;
26import com.android.vcard.VCardParser_V30;
27import com.android.vcard.exception.VCardException;
28import com.android.vcard.exception.VCardVersionException;
29import android.bluetooth.client.map.BluetoothMapBmessage.Status;
30import android.bluetooth.client.map.BluetoothMapBmessage.Type;
31import android.bluetooth.client.map.utils.BmsgTokenizer;
32import android.bluetooth.client.map.utils.BmsgTokenizer.Property;
33
34import java.io.ByteArrayInputStream;
35import java.io.IOException;
36import java.text.ParseException;
37
38class BluetoothMapBmessageParser {
39
40    private final static String TAG = "BluetoothMapBmessageParser";
41
42    private final static String CRLF = "\r\n";
43
44    private final static Property BEGIN_BMSG = new Property("BEGIN", "BMSG");
45    private final static Property END_BMSG = new Property("END", "BMSG");
46
47    private final static Property BEGIN_VCARD = new Property("BEGIN", "VCARD");
48    private final static Property END_VCARD = new Property("END", "VCARD");
49
50    private final static Property BEGIN_BENV = new Property("BEGIN", "BENV");
51    private final static Property END_BENV = new Property("END", "BENV");
52
53    private final static Property BEGIN_BBODY = new Property("BEGIN", "BBODY");
54    private final static Property END_BBODY = new Property("END", "BBODY");
55
56    private final static Property BEGIN_MSG = new Property("BEGIN", "MSG");
57    private final static Property END_MSG = new Property("END", "MSG");
58
59    private final static int CRLF_LEN = 2;
60
61    /*
62     * length of "container" for 'message' in bmessage-body-content:
63     * BEGIN:MSG<CRLF> + <CRLF> + END:MSG<CRFL>
64     */
65    private final static int MSG_CONTAINER_LEN = 22;
66
67    private BmsgTokenizer mParser;
68
69    private final BluetoothMapBmessage mBmsg;
70
71    private BluetoothMapBmessageParser() {
72        mBmsg = new BluetoothMapBmessage();
73    }
74
75    static public BluetoothMapBmessage createBmessage(String str) {
76        BluetoothMapBmessageParser p = new BluetoothMapBmessageParser();
77
78        try {
79            p.parse(str);
80        } catch (IOException e) {
81            Log.e(TAG, "I/O exception when parsing bMessage", e);
82            return null;
83        } catch (ParseException e) {
84            Log.e(TAG, "Cannot parse bMessage", e);
85            return null;
86        }
87
88        return p.mBmsg;
89    }
90
91    private ParseException expected(Property... props) {
92        boolean first = true;
93        StringBuilder sb = new StringBuilder();
94
95        for (Property prop : props) {
96            if (!first) {
97                sb.append(" or ");
98            }
99            sb.append(prop);
100            first = false;
101        }
102
103        return new ParseException("Expected: " + sb.toString(), mParser.pos());
104    }
105
106    private void parse(String str) throws IOException, ParseException {
107
108        Property prop;
109
110        /*
111         * <bmessage-object>::= { "BEGIN:BMSG" <CRLF> <bmessage-property>
112         * [<bmessage-originator>]* <bmessage-envelope> "END:BMSG" <CRLF> }
113         */
114
115        mParser = new BmsgTokenizer(str + CRLF);
116
117        prop = mParser.next();
118        if (!prop.equals(BEGIN_BMSG)) {
119            throw expected(BEGIN_BMSG);
120        }
121
122        prop = parseProperties();
123
124        while (prop.equals(BEGIN_VCARD)) {
125
126            /* <bmessage-originator>::= <vcard> <CRLF> */
127
128            StringBuilder vcard = new StringBuilder();
129            prop = extractVcard(vcard);
130
131            VCardEntry entry = parseVcard(vcard.toString());
132            mBmsg.mOriginators.add(entry);
133        }
134
135        if (!prop.equals(BEGIN_BENV)) {
136            throw expected(BEGIN_BENV);
137        }
138
139        prop = parseEnvelope(1);
140
141        if (!prop.equals(END_BMSG)) {
142            throw expected(END_BENV);
143        }
144
145        /*
146         * there should be no meaningful data left in stream here so we just
147         * ignore whatever is left
148         */
149
150        mParser = null;
151    }
152
153    private Property parseProperties() throws ParseException {
154
155        Property prop;
156
157        /*
158         * <bmessage-property>::=<bmessage-version-property>
159         * <bmessage-readstatus-property> <bmessage-type-property>
160         * <bmessage-folder-property> <bmessage-version-property>::="VERSION:"
161         * <common-digit>*"."<common-digit>* <CRLF>
162         * <bmessage-readstatus-property>::="STATUS:" 'readstatus' <CRLF>
163         * <bmessage-type-property>::="TYPE:" 'type' <CRLF>
164         * <bmessage-folder-property>::="FOLDER:" 'foldername' <CRLF>
165         */
166
167        do {
168            prop = mParser.next();
169
170            if (prop.name.equals("VERSION")) {
171                mBmsg.mBmsgVersion = prop.value;
172
173            } else if (prop.name.equals("STATUS")) {
174                for (Status s : Status.values()) {
175                    if (prop.value.equals(s.toString())) {
176                        mBmsg.mBmsgStatus = s;
177                        break;
178                    }
179                }
180
181            } else if (prop.name.equals("TYPE")) {
182                for (Type t : Type.values()) {
183                    if (prop.value.equals(t.toString())) {
184                        mBmsg.mBmsgType = t;
185                        break;
186                    }
187                }
188
189            } else if (prop.name.equals("FOLDER")) {
190                mBmsg.mBmsgFolder = prop.value;
191
192            }
193
194        } while (!prop.equals(BEGIN_VCARD) && !prop.equals(BEGIN_BENV));
195
196        return prop;
197    }
198
199    private Property parseEnvelope(int level) throws IOException, ParseException {
200
201        Property prop;
202
203        /*
204         * we can support as many nesting level as we want, but MAP spec clearly
205         * defines that there should be no more than 3 levels. so we verify it
206         * here.
207         */
208
209        if (level > 3) {
210            throw new ParseException("bEnvelope is nested more than 3 times", mParser.pos());
211        }
212
213        /*
214         * <bmessage-envelope> ::= { "BEGIN:BENV" <CRLF> [<bmessage-recipient>]*
215         * <bmessage-envelope> | <bmessage-content> "END:BENV" <CRLF> }
216         */
217
218        prop = mParser.next();
219
220        while (prop.equals(BEGIN_VCARD)) {
221
222            /* <bmessage-originator>::= <vcard> <CRLF> */
223
224            StringBuilder vcard = new StringBuilder();
225            prop = extractVcard(vcard);
226
227            if (level == 1) {
228                VCardEntry entry = parseVcard(vcard.toString());
229                mBmsg.mRecipients.add(entry);
230            }
231        }
232
233        if (prop.equals(BEGIN_BENV)) {
234            prop = parseEnvelope(level + 1);
235
236        } else if (prop.equals(BEGIN_BBODY)) {
237            prop = parseBody();
238
239        } else {
240            throw expected(BEGIN_BENV, BEGIN_BBODY);
241        }
242
243        if (!prop.equals(END_BENV)) {
244            throw expected(END_BENV);
245        }
246
247        return mParser.next();
248    }
249
250    private Property parseBody() throws IOException, ParseException {
251
252        Property prop;
253
254        /*
255         * <bmessage-content>::= { "BEGIN:BBODY"<CRLF> [<bmessage-body-part-ID>
256         * <CRLF>] <bmessage-body-property> <bmessage-body-content>* <CRLF>
257         * "END:BBODY"<CRLF> } <bmessage-body-part-ID>::="PARTID:" 'Part-ID'
258         * <bmessage-body-property>::=[<bmessage-body-encoding-property>]
259         * [<bmessage-body-charset-property>]
260         * [<bmessage-body-language-property>]
261         * <bmessage-body-content-length-property>
262         * <bmessage-body-encoding-property>::="ENCODING:"'encoding' <CRLF>
263         * <bmessage-body-charset-property>::="CHARSET:"'charset' <CRLF>
264         * <bmessage-body-language-property>::="LANGUAGE:"'language' <CRLF>
265         * <bmessage-body-content-length-property>::= "LENGTH:" <common-digit>*
266         * <CRLF>
267         */
268
269        do {
270            prop = mParser.next();
271
272            if (prop.name.equals("PARTID")) {
273            } else if (prop.name.equals("ENCODING")) {
274                mBmsg.mBbodyEncoding = prop.value;
275
276            } else if (prop.name.equals("CHARSET")) {
277                mBmsg.mBbodyCharset = prop.value;
278
279            } else if (prop.name.equals("LANGUAGE")) {
280                mBmsg.mBbodyLanguage = prop.value;
281
282            } else if (prop.name.equals("LENGTH")) {
283                try {
284                    mBmsg.mBbodyLength = Integer.valueOf(prop.value);
285                } catch (NumberFormatException e) {
286                    throw new ParseException("Invalid LENGTH value", mParser.pos());
287                }
288
289            }
290
291        } while (!prop.equals(BEGIN_MSG));
292
293        /*
294         * <bmessage-body-content>::={ "BEGIN:MSG"<CRLF> 'message'<CRLF>
295         * "END:MSG"<CRLF> }
296         */
297
298        int messageLen = mBmsg.mBbodyLength - MSG_CONTAINER_LEN;
299        int offset = messageLen + CRLF_LEN;
300        int restartPos = mParser.pos() + offset;
301
302        /*
303         * length is specified in bytes so we need to convert from unicode
304         * string back to bytes array
305         */
306
307        String remng = mParser.remaining();
308        byte[] data = remng.getBytes();
309
310        /* restart parsing from after 'message'<CRLF> */
311        mParser = new BmsgTokenizer(new String(data, offset, data.length - offset), restartPos);
312
313        prop = mParser.next(true);
314
315        if (prop != null) {
316            if (prop.equals(END_MSG)) {
317                mBmsg.mMessage = new String(data, 0, messageLen);
318            } else {
319                /* Handle possible exception for incorrect LENGTH value
320                 * from MSE while parsing  GET Message response */
321                Log.e(TAG, "Prop Invalid: "+ prop.toString());
322                Log.e(TAG, "Possible Invalid LENGTH value");
323                throw expected(END_MSG);
324            }
325        } else {
326
327            data = null;
328
329            /*
330             * now we check if bMessage can be parsed if LENGTH is handled as
331             * number of characters instead of number of bytes
332             */
333            if (offset < 0 || offset > remng.length()) {
334                /* Handle possible exception for incorrect LENGTH value
335                 * from MSE while parsing  GET Message response */
336                throw new ParseException("Invalid LENGTH value", mParser.pos());
337            }
338
339            Log.w(TAG, "byte LENGTH seems to be invalid, trying with char length");
340
341            mParser = new BmsgTokenizer(remng.substring(offset));
342
343            prop = mParser.next();
344
345            if (!prop.equals(END_MSG)) {
346                throw expected(END_MSG);
347            }
348
349            mBmsg.mMessage = remng.substring(0, messageLen);
350        }
351
352        prop = mParser.next();
353
354        if (!prop.equals(END_BBODY)) {
355            throw expected(END_BBODY);
356        }
357
358        return mParser.next();
359    }
360
361    private Property extractVcard(StringBuilder out) throws IOException, ParseException {
362        Property prop;
363
364        out.append(BEGIN_VCARD).append(CRLF);
365
366        do {
367            prop = mParser.next();
368            out.append(prop).append(CRLF);
369        } while (!prop.equals(END_VCARD));
370
371        return mParser.next();
372    }
373
374    private class VcardHandler implements VCardEntryHandler {
375
376        VCardEntry vcard;
377
378        @Override
379        public void onStart() {
380        }
381
382        @Override
383        public void onEntryCreated(VCardEntry entry) {
384            vcard = entry;
385        }
386
387        @Override
388        public void onEnd() {
389        }
390    };
391
392    private VCardEntry parseVcard(String str) throws IOException, ParseException {
393        VCardEntry vcard = null;
394
395        try {
396            VCardParser p = new VCardParser_V21();
397            VCardEntryConstructor c = new VCardEntryConstructor();
398            VcardHandler handler = new VcardHandler();
399            c.addEntryHandler(handler);
400            p.addInterpreter(c);
401            p.parse(new ByteArrayInputStream(str.getBytes()));
402
403            vcard = handler.vcard;
404
405        } catch (VCardVersionException e1) {
406
407            try {
408                VCardParser p = new VCardParser_V30();
409                VCardEntryConstructor c = new VCardEntryConstructor();
410                VcardHandler handler = new VcardHandler();
411                c.addEntryHandler(handler);
412                p.addInterpreter(c);
413                p.parse(new ByteArrayInputStream(str.getBytes()));
414
415                vcard = handler.vcard;
416
417            } catch (VCardVersionException e2) {
418                // will throw below
419            } catch (VCardException e2) {
420                // will throw below
421            }
422
423        } catch (VCardException e1) {
424            // will throw below
425        }
426
427        if (vcard == null) {
428            throw new ParseException("Cannot parse vCard object (neither 2.1 nor 3.0?)",
429                    mParser.pos());
430        }
431
432        return vcard;
433    }
434}
435