/* * Copyright (C) 2007 Esmertec AG. * Copyright (C) 2007 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 com.android.im.imps; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.android.im.imps.Primitive.TransactionMode; /** * PTS/SMS encoded IMPS messages parser. Only response transactions and * server initiated requests are supported. */ public class PtsPrimitiveParser implements PrimitiveParser { // WVaaBBcccDD <parameters> // aa - version number; 12 for 1.2, 13 for 1.3; "XX" for version discovery // BB - message type, case insensitive // ccc - transaction id in range 0-999 without preceding zero // DD - multiple SMSes identifier private static final Pattern sPreamplePattern = Pattern.compile("\\AWV(\\d{2})(\\p{Alpha}{2})(\\d{1,3})(\\p{Alpha}{2})?(\\z| .*)"); private char mReadBuf[] = new char[256]; private StringBuilder mStringBuf = new StringBuilder(); private int mPos; private static int UNCERTAIN_GROUP_SIZE = -1; public Primitive parse(InputStream in) throws ParserException, IOException { // assuming PTS data is always short BufferedReader reader = new BufferedReader( new InputStreamReader(in, "UTF-8"), 128); mStringBuf.setLength(0); mPos = 0; int len; while ((len = reader.read(mReadBuf)) != -1) { mStringBuf.append(mReadBuf, 0, len); } return parsePrim(); } private Primitive parsePrim() throws ParserException { Matcher m = sPreamplePattern.matcher(mStringBuf); if (!m.matches()) { throw new ParserException("Invalid PTS encoded message"); } Primitive p = new Primitive(); // TODO: handle WV version in m.group(1) String type = m.group(2).toUpperCase(); String transactionType = PtsCodes.getTransaction(type); if (transactionType == null) { throw new ParserException("Unrecognized transaction code " + type); } p.setContentElement(transactionType); if (PtsCodes.isServerRequestCode(type)) { p.setTransactionMode(TransactionMode.Request); } else { p.setTransactionMode(TransactionMode.Response); } p.setTransactionId(m.group(3)); mPos = m.start(5); if (mPos < mStringBuf.length()) { match(' '); HashMap<String, ParamValue> params = parseParams(); for (Entry<String, ParamValue> param : params.entrySet()) { translateParam(p, param.getKey(), param.getValue()); } } return p; } private static HashMap<String, Integer> sInfoElemTypeMap; private static final int ELEM_OTHER_SIMPLE = 0; private static final int ELEM_SESSION_ID = 1; private static final int ELEM_RESULT = 2; private static final int ELEM_ALL_FUNCTIONS = 3; private static final int ELEM_NOT_AVAIL_FUNCS = 4; private static final int ELEM_CAPABILITY_LIST = 5; private static final int ELEM_CONTACT_LIST = 6; private static final int ELEM_DEFAULT_CONTACT_LIST = 7; private static final int ELEM_USER_NICK_LIST = 8; private static final int ELEM_CONTACT_LIST_PROPS = 9; private static final int ELEM_PRESENCE = 10; /* private static final int ELEM_RESULT_CLIST = 3; private static final int ELEM_RESULT_DOMAIN = 4; private static final int ELEM_RESULT_GROUP = 5; private static final int ELEM_RESULT_MSGID = 6; private static final int ELEM_RESULT_SCRNAME = 7; private static final int ELEM_RESULT_USER = 8; */ static { sInfoElemTypeMap = new HashMap<String, Integer>(); sInfoElemTypeMap.put(PtsCodes.SessionID, ELEM_SESSION_ID); sInfoElemTypeMap.put(PtsCodes.Status, ELEM_RESULT); sInfoElemTypeMap.put(PtsCodes.NotAvailableFunctions, ELEM_NOT_AVAIL_FUNCS); sInfoElemTypeMap.put(PtsCodes.AllFunctions, ELEM_ALL_FUNCTIONS); sInfoElemTypeMap.put(PtsCodes.AgreedCapabilityList, ELEM_CAPABILITY_LIST); sInfoElemTypeMap.put(PtsCodes.ContactList, ELEM_CONTACT_LIST); sInfoElemTypeMap.put(PtsCodes.DefaultContactList, ELEM_DEFAULT_CONTACT_LIST); sInfoElemTypeMap.put(PtsCodes.UserNickList, ELEM_USER_NICK_LIST); sInfoElemTypeMap.put(PtsCodes.ContactListProps, ELEM_CONTACT_LIST_PROPS); sInfoElemTypeMap.put(PtsCodes.Presence, ELEM_PRESENCE); } private static void translateParam(Primitive p, String elemCode, ParamValue elemValue) throws ParserException { int type; elemCode = elemCode.toUpperCase(); // FIXME: Should be refactored when we had concrete situation of the null value case if (elemValue == null) { throw new ParserException("Parameter " + elemCode + " must have value."); } if (sInfoElemTypeMap.containsKey(elemCode)) { type = sInfoElemTypeMap.get(elemCode); /* if (type == ELEM_RESULT_CLIST && p.getType().equals(ImpsTags.Login_Response)) { // Fix up DigestSchema which shares a same code with // ContactListID. It appears only in Login_Response. type = ELEM_OTHER_SIMPLE; } */ } else { type = ELEM_OTHER_SIMPLE; } switch (type) { case ELEM_SESSION_ID: if (elemValue.mStrValue == null) { throw new ParserException("Element SessionID must have string value!"); } if (p.getType().equals(ImpsTags.Login_Response)) { p.addElement(ImpsTags.SessionID, elemValue.mStrValue); } else { p.setSession(elemValue.mStrValue); } break; case ELEM_RESULT: // ST=<StatusCode> // ST=(<StatusCode>,<Description>) PrimitiveElement result = p.addElement(ImpsTags.Result); if (elemValue.mStrValue != null) { result.addChild(ImpsTags.Code, elemValue.mStrValue); } else { checkGroupValue(elemValue.mValueGroup, 2); result.addChild(ImpsTags.Code, elemValue.mValueGroup.get(0).mStrValue); result.addChild(ImpsTags.Description, elemValue.mValueGroup.get(1).mStrValue); } break; case ELEM_ALL_FUNCTIONS: case ELEM_NOT_AVAIL_FUNCS: p.addElement(translateServiceTree(elemCode, elemValue)); break; case ELEM_CAPABILITY_LIST: p.addElement(translateCapabilityList(elemValue)); break; case ELEM_CONTACT_LIST: if (elemValue.mStrValue != null) { p.addElement(ImpsTags.ContactList, elemValue.mStrValue); } else { checkGroupValue(elemValue.mValueGroup, UNCERTAIN_GROUP_SIZE); for (ParamValue value : elemValue.mValueGroup) { p.addElement(ImpsTags.ContactList, value.mStrValue); } } break; case ELEM_DEFAULT_CONTACT_LIST: if (elemValue.mStrValue == null) { throw new ParserException("Deafult Contact List must have string value!"); } p.addElement(ImpsTags.DefaultContactList, elemValue.mStrValue); break; case ELEM_USER_NICK_LIST: { checkGroupValue(elemValue.mValueGroup, UNCERTAIN_GROUP_SIZE); PrimitiveElement nicklistElem = p.addElement(ImpsTags.NickList); int groupSize = elemValue.mValueGroup.size(); for (int i = 0; i < groupSize; i++) { ArrayList<ParamValue> valueGroup = elemValue.mValueGroup.get(i).mValueGroup; checkGroupValue(valueGroup, 2); String nickname = valueGroup.get(0).mStrValue; String address = valueGroup.get(1).mStrValue; if (nickname == null || address == null) { throw new ParserException("Null value found for NickName: " + nickname + "-" + address); } PrimitiveElement nicknameElem = nicklistElem.addChild(ImpsTags.NickName); nicknameElem.addChild(ImpsTags.Name, "".equals(nickname) ? null : nickname); nicknameElem.addChild(ImpsTags.UserID, address); } } break; case ELEM_CONTACT_LIST_PROPS: { checkGroupValue(elemValue.mValueGroup, UNCERTAIN_GROUP_SIZE); PrimitiveElement propertiesElem = p.addElement(ImpsTags.ContactListProperties); int groupSize = elemValue.mValueGroup.size(); for (int i = 0; i < groupSize; i++) { ArrayList<ParamValue> valueGroup = elemValue.mValueGroup.get(i).mValueGroup; checkGroupValue(valueGroup, 2); String name = valueGroup.get(0).mStrValue; String value = valueGroup.get(1).mStrValue; if (name == null || value == null) { throw new ParserException("Null value found for property: " + name + "-" + value); } if (PtsCodes.DisplayName.equals(name)) { name = ImpsConstants.DisplayName; } else if (PtsCodes.Default.equals(name)) { name = ImpsConstants.Default; } else { throw new ParserException("Unrecognized property " + name); } PrimitiveElement propertyElem = propertiesElem.addChild(ImpsTags.Property); propertyElem.addChild(ImpsTags.Name, name); propertyElem.addChild(ImpsTags.Value, value); } } break; case ELEM_PRESENCE: //PR=(<UserID>[,<PresenceSubList>]) //PR=((<UserID>[,<PresenceSubList>]),(<UserID>[,<PresenceSubList>])) checkGroupValue(elemValue.mValueGroup, UNCERTAIN_GROUP_SIZE); if (elemValue.mValueGroup.size() == 1) { // PR=(<UserID>) ParamValue value = elemValue.mValueGroup.get(0); if (value.mStrValue != null) { p.addElement(ImpsTags.Presence).addChild(ImpsTags.UserID, value.mStrValue); } else { // workaround for OZ server p.addElement(translatePresence(value.mValueGroup)); } } else { if (elemValue.mValueGroup.get(0).mStrValue == null) { // PR=((<UserID>[,<PresenceSubList>]),(<UserID>[,<PresenceSubList>])) int groupSize = elemValue.mValueGroup.size(); for (int i = 0; i < groupSize; i++) { ParamValue value = elemValue.mValueGroup.get(i); if (value.mStrValue != null) { p.addElement(ImpsTags.Presence).addChild(ImpsTags.UserID, value.mStrValue); } else { p.addElement(translatePresence(value.mValueGroup)); } } } else { // PR=(<UserID>,<PresenceSubList>) p.addElement(translatePresence(elemValue.mValueGroup)); } } break; case ELEM_OTHER_SIMPLE: p.addElement(translateSimpleElem(elemCode, elemValue)); break; default: throw new ParserException("Unsupported element " + elemValue); } } private static PrimitiveElement translatePresence(ArrayList<ParamValue> valueGroup) throws ParserException { checkGroupValue(valueGroup, UNCERTAIN_GROUP_SIZE); PrimitiveElement presence = new PrimitiveElement(ImpsTags.Presence); if (valueGroup.get(0).mStrValue == null) { throw new ParserException("UserID must have string value!"); } presence.addChild(ImpsTags.UserID, valueGroup.get(0).mStrValue); if (valueGroup.size() > 1) { // has presence sub list presence.addChild(translatePresenceSubList(valueGroup.get(1))); } return presence; } private static PrimitiveElement translatePresenceSubList(ParamValue value) throws ParserException { checkGroupValue(value.mValueGroup, UNCERTAIN_GROUP_SIZE); PrimitiveElement presenceSubList = new PrimitiveElement(ImpsTags.PresenceSubList); int groupSize = value.mValueGroup.size(); for (int i = 0; i < groupSize; i++) { ParamValue v = value.mValueGroup.get(i); if (v.mStrValue != null) { throw new ParserException("Unexpected string value for presence attribute"); } presenceSubList.addChild(translatePresenceAttribute(v.mValueGroup)); } return presenceSubList; } // <attribute>[,<qualifier>][,<value>] // <attribute>[,<qualifier>,<sub-attribute>] private static PrimitiveElement translatePresenceAttribute( ArrayList<ParamValue> valueGroup) throws ParserException { String type = valueGroup.get(0).mStrValue; if (type == null) { return null; } String tag = PtsCodes.getPresenceAttributeElement(type); if (tag == null) { return null; } PrimitiveElement paElem = new PrimitiveElement(tag); if (valueGroup.size() == 2) { // no qualifier translateAttributeValue(paElem, valueGroup.get(1), false); }else if (valueGroup.size() == 3) { // has qualifier, and it should has no group value ParamValue qualifierValue = valueGroup.get(1); if (qualifierValue.mStrValue == null) { throw new ParserException("Qualifier value can't be group value!"); } if (!"".equals(qualifierValue.mStrValue)) { paElem.addChild(ImpsTags.Qualifier, qualifierValue.mStrValue); } translateAttributeValue(paElem, valueGroup.get(2), true); } else { return null; } return paElem; } private static void translateAttributeValue(PrimitiveElement paElem, ParamValue v, boolean hasQualifier) throws ParserException { if (v.mStrValue == null) { // sub-attribute as value checkGroupValue(v.mValueGroup, UNCERTAIN_GROUP_SIZE); if (v.mValueGroup.get(0).mStrValue != null) { paElem.addChild(translatePresenceAttribute(v.mValueGroup)); } else { int groupSize = v.mValueGroup.size(); for (int i = 0; i < groupSize; i++) { ParamValue value = v.mValueGroup.get(i); if (value.mStrValue != null) { throw new ParserException("Presence Attribute value error!"); } checkGroupValue(value.mValueGroup, UNCERTAIN_GROUP_SIZE); paElem.addChild(translatePresenceAttribute(value.mValueGroup)); } } } else { // single simple value if (hasQualifier) { paElem.addChild(ImpsTags.PresenceValue, PtsCodes.getPAValue(v.mStrValue)); } else { paElem.setContents(PtsCodes.getPAValue(v.mStrValue)); } } } private static void checkGroupValue(ArrayList<ParamValue> valueGroup, int expectedGroupSize) throws ParserException { if (valueGroup == null || (expectedGroupSize != UNCERTAIN_GROUP_SIZE && valueGroup.size() != expectedGroupSize)) { throw new ParserException("Invalid group value!"); } int groupSize = valueGroup.size(); for (int i = 0; i < groupSize; i++) { if (valueGroup.get(i) == null) { throw new ParserException("Invalid group value!"); } } } private static PrimitiveElement translateCapabilityList(ParamValue elemValue) throws ParserException { PrimitiveElement elem = new PrimitiveElement(ImpsTags.AgreedCapabilityList); ArrayList<ParamValue> params = elemValue.mValueGroup; if (params != null) { checkGroupValue(params, UNCERTAIN_GROUP_SIZE); int paramsSize = params.size(); for (int i = 0; i < paramsSize; i++) { ArrayList<ParamValue> capElemGroup = params.get(i).mValueGroup; checkGroupValue(capElemGroup, 2); String capElemCode = capElemGroup.get(0).mStrValue; String capElemName; if (capElemCode == null || (capElemName = PtsCodes.getCapElement(capElemCode)) == null) { throw new ParserException("Unknown capability element " + capElemCode); } String capElemValue = capElemGroup.get(1).mStrValue; if (capElemValue == null) { throw new ParserException("Illegal capability value for " + capElemCode); } capElemValue = PtsCodes.getCapValue(capElemValue); elem.addChild(capElemName, capElemValue); } } return elem; } private static PrimitiveElement translateServiceTree(String elemCode, ParamValue elemValue) throws ParserException { String elemName = PtsCodes.getElement(elemCode); PrimitiveElement elem = new PrimitiveElement(elemName); // TODO: translate the service tree. return elem; } private static PrimitiveElement translateSimpleElem(String elemCode, ParamValue value) throws ParserException { String elemName = PtsCodes.getElement(elemCode); if (elemName == null) { throw new ParserException("Unrecognized parameter " + elemCode); } PrimitiveElement elem = new PrimitiveElement(elemName); if (value.mStrValue != null) { elem.setContents(value.mStrValue); } else { throw new ParserException("Don't know how to handle parameters for " + elemName); } return elem; } private HashMap<String, ParamValue> parseParams() throws ParserException { int pos = mPos; StringBuilder buf = mStringBuf; int len = buf.length(); HashMap<String, ParamValue> ret = new HashMap<String, ParamValue>(); String paramName; ParamValue paramValue; while (pos < len) { int nameStart = pos; while (pos < len) { char ch = buf.charAt(pos); if (ch == ' ' || ch == '=') { break; } pos++; } if (nameStart == pos) { throw new ParserException("Missing parameter name near " + pos); } paramName = buf.substring(nameStart, pos); if (pos < len && buf.charAt(pos) == '=') { pos++; mPos = pos; paramValue = parseParamValue(); pos = mPos; } else { paramValue = null; } ret.put(paramName, paramValue); if (pos < len) { // more parameters ahead match(' '); pos = mPos; } } return ret; } private ParamValue parseParamValue() throws ParserException { int pos = mPos; StringBuilder buf = mStringBuf; int len = buf.length(); if (pos == len) { throw new ParserException("Missing parameter value near " + pos); } ParamValue value = new ParamValue(); char ch = buf.charAt(pos); if (ch == '(') { // value list pos++; ArrayList<ParamValue> valueGroup = new ArrayList<ParamValue>(); while (pos < len) { mPos = pos; valueGroup.add(parseParamValue()); pos = mPos; if (pos == len) { throw new ParserException("Unexpected parameter end"); } if (buf.charAt(pos) != ',') { break; } pos++; } mPos = pos; match(')'); if (valueGroup.isEmpty()) { throw new ParserException("Empty value group near " + mPos); } value.mValueGroup = valueGroup; } else { // single value if (ch == '"') { // quoted value pos++; StringBuilder escapedValue = new StringBuilder(); boolean quotedEnd = false; while (pos < len) { ch = buf.charAt(pos); pos++; if (ch == '"') { if (pos < len && buf.charAt(pos) == '"') { // "doubled" quote pos++; } else { quotedEnd = true; break; } } escapedValue.append(ch); } if (!quotedEnd) { throw new ParserException("Unexpected quoted parameter end"); } value.mStrValue = escapedValue.toString(); } else { int valueStart = pos; while (pos < len) { ch = buf.charAt(pos); if (ch == ',' || ch == ')' || ch == ' ') { break; } if ("\"(=&".indexOf(ch) != -1) { throw new ParserException("Special character " + ch + " must be quoted"); } pos++; } value.mStrValue = buf.substring(valueStart, pos); } mPos = pos; } return value; } private void match(char c) throws ParserException { if (mStringBuf.charAt(mPos) != c) { throw new ParserException("Expected " + c + " at pos " + mPos); } mPos++; } /** * Detect if this short message is a PTS encoded WV-primitive. */ public static boolean isPtsPrimitive(CharSequence msg) { if (msg == null) { return false; } Matcher m = sPreamplePattern.matcher(msg); return m.matches(); } static final class ParamValue { public String mStrValue; public ArrayList<ParamValue> mValueGroup; } }