diff options
Diffstat (limited to 'src/android/bluetooth/client/map/BluetoothMapBmessageParser.java')
-rw-r--r-- | src/android/bluetooth/client/map/BluetoothMapBmessageParser.java | 421 |
1 files changed, 421 insertions, 0 deletions
diff --git a/src/android/bluetooth/client/map/BluetoothMapBmessageParser.java b/src/android/bluetooth/client/map/BluetoothMapBmessageParser.java new file mode 100644 index 0000000..fa3d817 --- /dev/null +++ b/src/android/bluetooth/client/map/BluetoothMapBmessageParser.java @@ -0,0 +1,421 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth.client.map; + +import android.util.Log; + +import com.android.vcard.VCardEntry; +import com.android.vcard.VCardEntryConstructor; +import com.android.vcard.VCardEntryHandler; +import com.android.vcard.VCardParser; +import com.android.vcard.VCardParser_V21; +import com.android.vcard.VCardParser_V30; +import com.android.vcard.exception.VCardException; +import com.android.vcard.exception.VCardVersionException; +import android.bluetooth.client.map.BluetoothMapBmessage.Status; +import android.bluetooth.client.map.BluetoothMapBmessage.Type; +import android.bluetooth.client.map.utils.BmsgTokenizer; +import android.bluetooth.client.map.utils.BmsgTokenizer.Property; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.text.ParseException; + +class BluetoothMapBmessageParser { + + private final static String TAG = "BluetoothMapBmessageParser"; + + private final static String CRLF = "\r\n"; + + private final static Property BEGIN_BMSG = new Property("BEGIN", "BMSG"); + private final static Property END_BMSG = new Property("END", "BMSG"); + + private final static Property BEGIN_VCARD = new Property("BEGIN", "VCARD"); + private final static Property END_VCARD = new Property("END", "VCARD"); + + private final static Property BEGIN_BENV = new Property("BEGIN", "BENV"); + private final static Property END_BENV = new Property("END", "BENV"); + + private final static Property BEGIN_BBODY = new Property("BEGIN", "BBODY"); + private final static Property END_BBODY = new Property("END", "BBODY"); + + private final static Property BEGIN_MSG = new Property("BEGIN", "MSG"); + private final static Property END_MSG = new Property("END", "MSG"); + + private final static int CRLF_LEN = 2; + + /* + * length of "container" for 'message' in bmessage-body-content: + * BEGIN:MSG<CRLF> + <CRLF> + END:MSG<CRFL> + */ + private final static int MSG_CONTAINER_LEN = 22; + + private BmsgTokenizer mParser; + + private final BluetoothMapBmessage mBmsg; + + private BluetoothMapBmessageParser() { + mBmsg = new BluetoothMapBmessage(); + } + + static public BluetoothMapBmessage createBmessage(String str) { + BluetoothMapBmessageParser p = new BluetoothMapBmessageParser(); + + try { + p.parse(str); + } catch (IOException e) { + Log.e(TAG, "I/O exception when parsing bMessage", e); + return null; + } catch (ParseException e) { + Log.e(TAG, "Cannot parse bMessage", e); + return null; + } + + return p.mBmsg; + } + + private ParseException expected(Property... props) { + boolean first = true; + StringBuilder sb = new StringBuilder(); + + for (Property prop : props) { + if (!first) { + sb.append(" or "); + } + sb.append(prop); + first = false; + } + + return new ParseException("Expected: " + sb.toString(), mParser.pos()); + } + + private void parse(String str) throws IOException, ParseException { + + Property prop; + + /* + * <bmessage-object>::= { "BEGIN:BMSG" <CRLF> <bmessage-property> + * [<bmessage-originator>]* <bmessage-envelope> "END:BMSG" <CRLF> } + */ + + mParser = new BmsgTokenizer(str + CRLF); + + prop = mParser.next(); + if (!prop.equals(BEGIN_BMSG)) { + throw expected(BEGIN_BMSG); + } + + prop = parseProperties(); + + while (prop.equals(BEGIN_VCARD)) { + + /* <bmessage-originator>::= <vcard> <CRLF> */ + + StringBuilder vcard = new StringBuilder(); + prop = extractVcard(vcard); + + VCardEntry entry = parseVcard(vcard.toString()); + mBmsg.mOriginators.add(entry); + } + + if (!prop.equals(BEGIN_BENV)) { + throw expected(BEGIN_BENV); + } + + prop = parseEnvelope(1); + + if (!prop.equals(END_BMSG)) { + throw expected(END_BENV); + } + + /* + * there should be no meaningful data left in stream here so we just + * ignore whatever is left + */ + + mParser = null; + } + + private Property parseProperties() throws ParseException { + + Property prop; + + /* + * <bmessage-property>::=<bmessage-version-property> + * <bmessage-readstatus-property> <bmessage-type-property> + * <bmessage-folder-property> <bmessage-version-property>::="VERSION:" + * <common-digit>*"."<common-digit>* <CRLF> + * <bmessage-readstatus-property>::="STATUS:" 'readstatus' <CRLF> + * <bmessage-type-property>::="TYPE:" 'type' <CRLF> + * <bmessage-folder-property>::="FOLDER:" 'foldername' <CRLF> + */ + + do { + prop = mParser.next(); + + if (prop.name.equals("VERSION")) { + mBmsg.mBmsgVersion = prop.value; + + } else if (prop.name.equals("STATUS")) { + for (Status s : Status.values()) { + if (prop.value.equals(s.toString())) { + mBmsg.mBmsgStatus = s; + break; + } + } + + } else if (prop.name.equals("TYPE")) { + for (Type t : Type.values()) { + if (prop.value.equals(t.toString())) { + mBmsg.mBmsgType = t; + break; + } + } + + } else if (prop.name.equals("FOLDER")) { + mBmsg.mBmsgFolder = prop.value; + + } + + } while (!prop.equals(BEGIN_VCARD) && !prop.equals(BEGIN_BENV)); + + return prop; + } + + private Property parseEnvelope(int level) throws IOException, ParseException { + + Property prop; + + /* + * we can support as many nesting level as we want, but MAP spec clearly + * defines that there should be no more than 3 levels. so we verify it + * here. + */ + + if (level > 3) { + throw new ParseException("bEnvelope is nested more than 3 times", mParser.pos()); + } + + /* + * <bmessage-envelope> ::= { "BEGIN:BENV" <CRLF> [<bmessage-recipient>]* + * <bmessage-envelope> | <bmessage-content> "END:BENV" <CRLF> } + */ + + prop = mParser.next(); + + while (prop.equals(BEGIN_VCARD)) { + + /* <bmessage-originator>::= <vcard> <CRLF> */ + + StringBuilder vcard = new StringBuilder(); + prop = extractVcard(vcard); + + if (level == 1) { + VCardEntry entry = parseVcard(vcard.toString()); + mBmsg.mRecipients.add(entry); + } + } + + if (prop.equals(BEGIN_BENV)) { + prop = parseEnvelope(level + 1); + + } else if (prop.equals(BEGIN_BBODY)) { + prop = parseBody(); + + } else { + throw expected(BEGIN_BENV, BEGIN_BBODY); + } + + if (!prop.equals(END_BENV)) { + throw expected(END_BENV); + } + + return mParser.next(); + } + + private Property parseBody() throws IOException, ParseException { + + Property prop; + + /* + * <bmessage-content>::= { "BEGIN:BBODY"<CRLF> [<bmessage-body-part-ID> + * <CRLF>] <bmessage-body-property> <bmessage-body-content>* <CRLF> + * "END:BBODY"<CRLF> } <bmessage-body-part-ID>::="PARTID:" 'Part-ID' + * <bmessage-body-property>::=[<bmessage-body-encoding-property>] + * [<bmessage-body-charset-property>] + * [<bmessage-body-language-property>] + * <bmessage-body-content-length-property> + * <bmessage-body-encoding-property>::="ENCODING:"'encoding' <CRLF> + * <bmessage-body-charset-property>::="CHARSET:"'charset' <CRLF> + * <bmessage-body-language-property>::="LANGUAGE:"'language' <CRLF> + * <bmessage-body-content-length-property>::= "LENGTH:" <common-digit>* + * <CRLF> + */ + + do { + prop = mParser.next(); + + if (prop.name.equals("PARTID")) { + } else if (prop.name.equals("ENCODING")) { + mBmsg.mBbodyEncoding = prop.value; + + } else if (prop.name.equals("CHARSET")) { + mBmsg.mBbodyCharset = prop.value; + + } else if (prop.name.equals("LANGUAGE")) { + mBmsg.mBbodyLanguage = prop.value; + + } else if (prop.name.equals("LENGTH")) { + try { + mBmsg.mBbodyLength = Integer.valueOf(prop.value); + } catch (NumberFormatException e) { + throw new ParseException("Invalid LENGTH value", mParser.pos()); + } + + } + + } while (!prop.equals(BEGIN_MSG)); + + /* + * <bmessage-body-content>::={ "BEGIN:MSG"<CRLF> 'message'<CRLF> + * "END:MSG"<CRLF> } + */ + + int messageLen = mBmsg.mBbodyLength - MSG_CONTAINER_LEN; + int offset = messageLen + CRLF_LEN; + int restartPos = mParser.pos() + offset; + + /* + * length is specified in bytes so we need to convert from unicode + * string back to bytes array + */ + + String remng = mParser.remaining(); + byte[] data = remng.getBytes(); + + /* restart parsing from after 'message'<CRLF> */ + mParser = new BmsgTokenizer(new String(data, offset, data.length - offset), restartPos); + + prop = mParser.next(true); + + if (prop != null && prop.equals(END_MSG)) { + mBmsg.mMessage = new String(data, 0, messageLen); + } else { + + data = null; + + /* + * now we check if bMessage can be parsed if LENGTH is handled as + * number of characters instead of number of bytes + */ + + Log.w(TAG, "byte LENGTH seems to be invalid, trying with char length"); + + mParser = new BmsgTokenizer(remng.substring(offset)); + + prop = mParser.next(); + + if (!prop.equals(END_MSG)) { + throw expected(END_MSG); + } + + mBmsg.mMessage = remng.substring(0, messageLen); + } + + prop = mParser.next(); + + if (!prop.equals(END_BBODY)) { + throw expected(END_BBODY); + } + + return mParser.next(); + } + + private Property extractVcard(StringBuilder out) throws IOException, ParseException { + Property prop; + + out.append(BEGIN_VCARD).append(CRLF); + + do { + prop = mParser.next(); + out.append(prop).append(CRLF); + } while (!prop.equals(END_VCARD)); + + return mParser.next(); + } + + private class VcardHandler implements VCardEntryHandler { + + VCardEntry vcard; + + @Override + public void onStart() { + } + + @Override + public void onEntryCreated(VCardEntry entry) { + vcard = entry; + } + + @Override + public void onEnd() { + } + }; + + private VCardEntry parseVcard(String str) throws IOException, ParseException { + VCardEntry vcard = null; + + try { + VCardParser p = new VCardParser_V21(); + VCardEntryConstructor c = new VCardEntryConstructor(); + VcardHandler handler = new VcardHandler(); + c.addEntryHandler(handler); + p.addInterpreter(c); + p.parse(new ByteArrayInputStream(str.getBytes())); + + vcard = handler.vcard; + + } catch (VCardVersionException e1) { + + try { + VCardParser p = new VCardParser_V30(); + VCardEntryConstructor c = new VCardEntryConstructor(); + VcardHandler handler = new VcardHandler(); + c.addEntryHandler(handler); + p.addInterpreter(c); + p.parse(new ByteArrayInputStream(str.getBytes())); + + vcard = handler.vcard; + + } catch (VCardVersionException e2) { + // will throw below + } catch (VCardException e2) { + // will throw below + } + + } catch (VCardException e1) { + // will throw below + } + + if (vcard == null) { + throw new ParseException("Cannot parse vCard object (neither 2.1 nor 3.0?)", + mParser.pos()); + } + + return vcard; + } +} |