blob: dbaabc7de709bffdcacfad3bbb53d35451aecfac [file] [log] [blame]
/*
* 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;
}
}