FileDocCategorySizeDatePackage
PtsPrimitiveParser.javaAPI DocAndroid 1.5 API23953Wed May 06 22:42:46 BST 2009com.android.im.imps

PtsPrimitiveParser.java

/*
 * 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;
    }
}