/* * CDDL HEADER START * * The contents of this file are subject to the terms of the Common Development * and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * You can obtain a copy of the license at * src/com/vodafone360/people/VODAFONE.LICENSE.txt or * http://github.com/360/360-Engine-for-Android * See the License for the specific language governing permissions and * limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each file and * include the License file at src/com/vodafone360/people/VODAFONE.LICENSE.txt. * If applicable, add the following below this CDDL HEADER, with the fields * enclosed by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * Copyright 2010 Vodafone Sales & Services Ltd. All rights reserved. * Use is subject to license terms. */ package com.vodafone360.people.service.utils.hessian; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; import java.util.Vector; import java.util.zip.GZIPInputStream; import com.caucho.hessian.micro.MicroHessianInput; import com.vodafone360.people.datatypes.ActivityItem; import com.vodafone360.people.datatypes.AuthSessionHolder; import com.vodafone360.people.datatypes.BaseDataType; import com.vodafone360.people.datatypes.Contact; import com.vodafone360.people.datatypes.ContactChanges; import com.vodafone360.people.datatypes.ContactDetailDeletion; import com.vodafone360.people.datatypes.ContactListResponse; import com.vodafone360.people.datatypes.Conversation; import com.vodafone360.people.datatypes.ExternalResponseObject; import com.vodafone360.people.datatypes.Identity; import com.vodafone360.people.datatypes.IdentityDeletion; import com.vodafone360.people.datatypes.ItemList; import com.vodafone360.people.datatypes.PresenceList; import com.vodafone360.people.datatypes.PublicKeyDetails; import com.vodafone360.people.datatypes.PushAvailabilityEvent; import com.vodafone360.people.datatypes.PushChatConversationEvent; import com.vodafone360.people.datatypes.PushChatMessageEvent; import com.vodafone360.people.datatypes.PushClosedConversationEvent; import com.vodafone360.people.datatypes.PushEvent; import com.vodafone360.people.datatypes.ServerError; import com.vodafone360.people.datatypes.SimpleText; import com.vodafone360.people.datatypes.StatusMsg; import com.vodafone360.people.datatypes.SystemNotification; import com.vodafone360.people.datatypes.UserProfile; import com.vodafone360.people.engine.EngineManager.EngineId; import com.vodafone360.people.service.io.Request; import com.vodafone360.people.service.io.ResponseQueue.DecodedResponse; import com.vodafone360.people.service.io.rpg.PushMessageTypes; import com.vodafone360.people.service.io.rpg.RpgPushMessage; import com.vodafone360.people.utils.CloseUtils; import com.vodafone360.people.utils.LogUtils; /** * Hessian decoding . TODO: Currently casting every response to a Map, losing * for example push events "c0" which only contains a string. This may need a * fix. */ public class HessianDecoder { private static final String KEY_ACTIVITY_LIST = "activitylist"; private static final String KEY_AVAILABLE_IDENTITY_LIST = "availableidentitylist"; private static final String KEY_CONTACT_ID_LIST = "contactidlist"; private static final String KEY_CONTACT_LIST = "contactlist"; private static final String KEY_IDENTITY_LIST = "identitylist"; private static final String KEY_SESSION = "session"; private static final String KEY_USER_PROFILE = "userprofile"; private static final String KEY_USER_PROFILE_LIST = "userprofilelist"; /** * The MicroHessianInput is here declared as member and will be reused * instead of making new instances on every need */ private MicroHessianInput mMicroHessianInput = new MicroHessianInput(); /** * * Parse Hessian encoded byte array placing parsed contents into List. * * @param requestId The request ID that the response was received for. * @param data byte array containing Hessian encoded data * @param type Event type Shows whether we have a push or common message type. * @param isZipped True if the response is gzipped, otherwise false. * @param engineId The engine ID the response should be reported back to. * * @return The response containing the decoded objects. * * @throws IOException Thrown if there is something wrong with reading the (gzipped) hessian encoded input stream. * */ public DecodedResponse decodeHessianByteArray(int requestId, byte[] data, Request.Type type, boolean isZipped, EngineId engineId) throws IOException { InputStream is = null; InputStream bis = null; if (isZipped == true) { LogUtils.logV("HessianDecoder.decodeHessianByteArray() Handle zipped data"); bis = new ByteArrayInputStream(data); is = new GZIPInputStream(bis, data.length); } else { LogUtils.logV("HessianDecoder.decodeHessianByteArray() Handle non-zipped data"); is = new ByteArrayInputStream(data); } DecodedResponse response = null; mMicroHessianInput.init(is); LogUtils.logV("HessianDecoder.decodeHessianByteArray() Begin Hessian decode"); try { response = decodeResponse(is, requestId, type, isZipped, engineId); } catch (IOException e) { LogUtils.logE("HessianDecoder.decodeHessianByteArray() " + "IOException during decodeResponse", e); } CloseUtils.close(bis); CloseUtils.close(is); return response; } @SuppressWarnings("unchecked") public Hashtable<String, Object> decodeHessianByteArrayToHashtable(byte[] data) throws IOException { InputStream is = new ByteArrayInputStream(data); mMicroHessianInput.init(is); Object obj = null; obj = mMicroHessianInput.decodeTag(); if (obj instanceof Hashtable) { return (Hashtable<String, Object>)obj; } else { return null; } } /** * * * * @param is * @param requestId * @param type * @param isZipped * @param engineId * * @return * * @throws IOException */ @SuppressWarnings("unchecked") private DecodedResponse decodeResponse(InputStream is, int requestId, Request.Type type, boolean isZipped, EngineId engineId) throws IOException { boolean usesReplyTag = false; int responseType = DecodedResponse.ResponseType.UNKNOWN.ordinal(); List<BaseDataType> resultList = new ArrayList<BaseDataType>(); mMicroHessianInput.init(is); // skip start int tag = is.read(); // initial map tag or fail if (tag == 'r') { // reply / response is.read(); // read major and minor is.read(); tag = is.read(); // read next tag usesReplyTag = true; } if (tag == -1) { return null; } // check for fail // read reason string and throw exception if (tag == 'f') { ServerError zybErr = new ServerError(mMicroHessianInput.readFault().errString()); resultList.add(zybErr); DecodedResponse decodedResponse = new DecodedResponse(requestId, resultList, engineId, DecodedResponse.ResponseType.SERVER_ERROR.ordinal()); return decodedResponse; } // handle external response // this is not wrapped up in a hashtable if (type == Request.Type.EXTERNAL_RPG_RESPONSE) { LogUtils.logV("HessianDecoder.decodeResponse() EXTERNAL_RPG_RESPONSE"); if (tag != 'I') { LogUtils.logE("HessianDecoder.decodeResponse() " + "tag!='I' Unexpected Hessian type:" + tag); } parseExternalResponse(resultList, is, tag); DecodedResponse decodedResponse = new DecodedResponse(requestId, resultList, engineId, DecodedResponse.ResponseType.SERVER_ERROR.ordinal()); return decodedResponse; } // internal response: should contain a Map type - i.e. Hashtable if (tag != 'M') { LogUtils .logE("HessianDecoder.decodeResponse() tag!='M' Unexpected Hessian type:" + tag); throw new IOException("Unexpected Hessian type"); } else if ((type == Request.Type.COMMON) || (type == Request.Type.SIGN_IN) || // if we have a common request or sign in request (type == Request.Type.GET_MY_IDENTITIES) || (type == Request.Type.GET_AVAILABLE_IDENTITIES)) { Hashtable<String, Object> map = (Hashtable<String, Object>)mMicroHessianInput .readHashMap(tag); if (null == map) { return null; } if (map.containsKey(KEY_SESSION)) { AuthSessionHolder auth = new AuthSessionHolder(); Hashtable<String, Object> authHash = (Hashtable<String, Object>)map .get(KEY_SESSION); resultList.add(auth.createFromHashtable(authHash)); responseType = DecodedResponse.ResponseType.LOGIN_RESPONSE.ordinal(); } else if (map.containsKey(KEY_CONTACT_LIST)) { // contact list getContacts(resultList, ((Vector<?>)map.get(KEY_CONTACT_LIST))); responseType = DecodedResponse.ResponseType.GET_CONTACTCHANGES_RESPONSE.ordinal(); } else if (map.containsKey(KEY_USER_PROFILE_LIST)) { Vector<Hashtable<String, Object>> upVect = (Vector<Hashtable<String, Object>>)map .get(KEY_USER_PROFILE_LIST); for (Hashtable<String, Object> obj : upVect) { resultList.add(UserProfile.createFromHashtable(obj)); } responseType = DecodedResponse.ResponseType.GETME_RESPONSE.ordinal(); } else if (map.containsKey(KEY_USER_PROFILE)) { Hashtable<String, Object> userProfileHash = (Hashtable<String, Object>)map .get(KEY_USER_PROFILE); resultList.add(UserProfile.createFromHashtable(userProfileHash)); responseType = DecodedResponse.ResponseType.GETME_RESPONSE.ordinal(); } else if ((map.containsKey(KEY_IDENTITY_LIST)) // we have identity items in the map which we can parse || (map.containsKey(KEY_AVAILABLE_IDENTITY_LIST))) { int identityType = 0; Vector<Hashtable<String, Object>> idcap = null; if (map.containsKey(KEY_IDENTITY_LIST)) { idcap = (Vector<Hashtable<String, Object>>)map.get(KEY_IDENTITY_LIST); identityType = BaseDataType.MY_IDENTITY_DATA_TYPE; responseType = DecodedResponse.ResponseType.GET_MY_IDENTITIES_RESPONSE.ordinal(); } else { idcap = (Vector<Hashtable<String, Object>>)map.get(KEY_AVAILABLE_IDENTITY_LIST); identityType = BaseDataType.AVAILABLE_IDENTITY_DATA_TYPE; responseType = DecodedResponse.ResponseType.GET_AVAILABLE_IDENTITIES_RESPONSE.ordinal(); } for (Hashtable<String, Object> obj : idcap) { Identity id = new Identity(identityType); resultList.add(id.createFromHashtable(obj)); } } else if (type == Request.Type.GET_AVAILABLE_IDENTITIES) { // we have an available identities response, but it is empty responseType = DecodedResponse.ResponseType.GET_AVAILABLE_IDENTITIES_RESPONSE.ordinal(); } else if (type == Request.Type.GET_MY_IDENTITIES) { // we have a my identities response, but it is empty responseType = DecodedResponse.ResponseType.GET_MY_IDENTITIES_RESPONSE.ordinal(); } else if (map.containsKey(KEY_ACTIVITY_LIST)) { Vector<Hashtable<String, Object>> activityList = (Vector<Hashtable<String, Object>>)map .get(KEY_ACTIVITY_LIST); for (Hashtable<String, Object> obj : activityList) { resultList.add(ActivityItem.createFromHashtable(obj)); } responseType = DecodedResponse.ResponseType.GET_ACTIVITY_RESPONSE.ordinal(); } } else if ((type != Request.Type.COMMON) && (type != Request.Type.SIGN_IN)) { // get initial hash table // TODO: we cast every response to a Map, losing e.g. push event // "c0" which only contains a string - to fix Hashtable<String, Object> hash = (Hashtable<String, Object>)mMicroHessianInput .decodeType(tag); responseType = decodeResponseByRequestType(resultList, hash, type); } if (usesReplyTag) { is.read(); // read the last 'z' } DecodedResponse decodedResponse = new DecodedResponse(requestId, resultList, engineId, responseType); return decodedResponse; } private void parseExternalResponse(List<BaseDataType> clist, InputStream is, int tag) throws IOException { mMicroHessianInput.init(is); ExternalResponseObject resp = new ExternalResponseObject(); // we already read the 'I' in the decodeResponse()-method // now we read and check the response code if (mMicroHessianInput.readInt(tag) != 200) { return; } try { resp.mMimeType = mMicroHessianInput.readString(); } catch (IOException ioe) { LogUtils.logE("Failed to parse hessian string."); return; } // read data - could be gzipped try { resp.mBody = mMicroHessianInput.readBytes(); } catch (IOException ioe) { LogUtils.logE("Failed to read bytes."); return; } LogUtils.logI("HessianDecoder.parseExternalResponse()" + " Parsed external object with length: " + resp.mBody.length); clist.add(resp); } /** * * Parses the hashtables retrieved from the hessian payload that came from the server and * returns a type for it. * * @param clist The list that will be populated with the data types. * @param hash The hash table that contains the parsed date returned by the backend. * @param type The type of the request that was sent, e.g. get contacts changes. * * @return The type of the response that was parsed (to be found in DecodedResponse.ResponseType). * */ private int decodeResponseByRequestType(List<BaseDataType> clist, Hashtable<String, Object> hash, Request.Type type) { int responseType = DecodedResponse.ResponseType.UNKNOWN.ordinal(); switch (type) { case CONTACT_CHANGES_OR_UPDATES: responseType = DecodedResponse.ResponseType.GET_CONTACTCHANGES_RESPONSE.ordinal(); // create ContactChanges ContactChanges contChanges = new ContactChanges(); contChanges = contChanges.createFromHashtable(hash); clist.add(contChanges); break; case ADD_CONTACT: clist.add(Contact.createFromHashtable(hash)); responseType = DecodedResponse.ResponseType.ADD_CONTACT_RESPONSE.ordinal(); break; case SIGN_UP: clist.add(Contact.createFromHashtable(hash)); responseType = DecodedResponse.ResponseType.SIGNUP_RESPONSE.ordinal(); break; case RETRIEVE_PUBLIC_KEY: // AA define new object type clist.add(PublicKeyDetails.createFromHashtable(hash)); responseType = DecodedResponse.ResponseType.RETRIEVE_PUBLIC_KEY_RESPONSE.ordinal(); break; case CONTACT_DELETE: ContactListResponse cresp = new ContactListResponse(); cresp.createFromHashTable(hash); // add ids @SuppressWarnings("unchecked") Vector<Long> contactIds = (Vector<Long>)hash.get(KEY_CONTACT_ID_LIST); if (contactIds != null) { for (Long cid : contactIds) { cresp.mContactIdList.add((cid).intValue()); } } clist.add(cresp); responseType = DecodedResponse.ResponseType.DELETE_CONTACT_RESPONSE.ordinal(); break; case CONTACT_DETAIL_DELETE: ContactDetailDeletion cdel = new ContactDetailDeletion(); clist.add(cdel.createFromHashtable(hash)); responseType = DecodedResponse.ResponseType.DELETE_CONTACT_DETAIL_RESPONSE.ordinal(); break; case CONTACT_GROUP_RELATION_LIST: ItemList groupRelationList = new ItemList(ItemList.Type.contact_group_relation); groupRelationList.populateFromHashtable(hash); clist.add(groupRelationList); responseType = DecodedResponse.ResponseType.GET_CONTACT_GROUP_RELATIONS_RESPONSE.ordinal(); break; case CONTACT_GROUP_RELATIONS: ItemList groupRelationsList = new ItemList(ItemList.Type.contact_group_relations); groupRelationsList.populateFromHashtable(hash); clist.add(groupRelationsList); responseType = DecodedResponse.ResponseType.GET_CONTACT_GROUP_RELATIONS_RESPONSE.ordinal(); break; case DELETE_CONTACT_GROUP_RELATIONS: // The hessian data sent by the backend is of the form // r{1}{0}Mt{0}{0}zz. The MicroHessianInput always skips the 2 bytes // after the type. This doesn't seem to be handling the case where // the type is of length zero. Due to this, after decoding, the hash // doesn't contain any elements/keys. Due to this, we are hardcoding // the status to true here. StatusMsg statusMsg = new StatusMsg(); statusMsg.mStatus = true; clist.add(statusMsg); responseType = DecodedResponse.ResponseType.UNKNOWN.ordinal(); break; case GROUP_LIST: ItemList zyblist = new ItemList(ItemList.Type.group_privacy); zyblist.populateFromHashtable(hash); clist.add(zyblist); responseType = DecodedResponse.ResponseType.GET_GROUPS_RESPONSE.ordinal(); break; case ITEM_LIST_OF_LONGS: ItemList listOfLongs = new ItemList(ItemList.Type.long_value); listOfLongs.populateFromHashtable(hash); clist.add(listOfLongs); responseType = DecodedResponse.ResponseType.UNKNOWN.ordinal(); // TODO break; case STATUS_LIST: // TODO status and status list are used by many requests as a type. each request should have its own type however! ItemList zybstatlist = new ItemList(ItemList.Type.status_msg); zybstatlist.populateFromHashtable(hash); clist.add(zybstatlist); responseType = DecodedResponse.ResponseType.UNKNOWN.ordinal(); // TODO break; case STATUS: StatusMsg s = new StatusMsg(); s.mStatus = true; clist.add(s); responseType = DecodedResponse.ResponseType.UNKNOWN.ordinal(); // TODO break; case TEXT_RESPONSE_ONLY: Object val = hash.get("result"); if (val != null && val instanceof String) { SimpleText txt = new SimpleText(); txt.addText((String)val); clist.add(txt); } responseType = DecodedResponse.ResponseType.UNKNOWN.ordinal(); // TODO break; case EXPECTING_STATUS_ONLY: StatusMsg statMsg = new StatusMsg(); clist.add(statMsg.createFromHashtable(hash)); responseType = DecodedResponse.ResponseType.UNKNOWN.ordinal(); // TODO break; case PRESENCE_LIST: PresenceList mPresenceList = new PresenceList(); mPresenceList.createFromHashtable(hash); clist.add(mPresenceList); responseType = DecodedResponse.ResponseType.GET_PRESENCE_RESPONSE.ordinal(); break; case PUSH_MSG: // parse content of RPG Push msg parsePushMessage(clist, hash); responseType = DecodedResponse.ResponseType.PUSH_MESSAGE.ordinal(); break; case CREATE_CONVERSATION: Conversation mConversation = new Conversation(); mConversation.createFromHashtable(hash); clist.add(mConversation); responseType = DecodedResponse.ResponseType.CREATE_CONVERSATION_RESPONSE.ordinal(); break; case DELETE_IDENTITY: IdentityDeletion mIdenitityDeletion = new IdentityDeletion(); clist.add(mIdenitityDeletion.createFromHashtable(hash)); responseType = DecodedResponse.ResponseType.DELETE_IDENTITY_RESPONSE .ordinal(); break; default: LogUtils.logE("HessianDecoder.decodeResponseByRequestType() Unhandled type[" + type.name() + "]"); } return responseType; } private void getContacts(List<BaseDataType> clist, Vector<?> cont) { for (Object obj : cont) { @SuppressWarnings("unchecked") Hashtable<String, Object> hash = (Hashtable<String, Object>)obj; clist.add(Contact.createFromHashtable(hash)); } } private void parsePushMessage(List<BaseDataType> list, Hashtable<String, Object> hash) { RpgPushMessage push = RpgPushMessage.createFromHashtable(hash); parsePushPayload(push, list); } private void parsePushPayload(RpgPushMessage msg, List<BaseDataType> list) { // convert push msg type string to PushMsgType PushMessageTypes type = msg.mType; EngineId engineId = EngineId.UNDEFINED; if (type != null) { switch (type) { case CHAT_MESSAGE: LogUtils.logV("Parse incomming chat_message"); engineId = EngineId.PRESENCE_ENGINE; list.add(new PushChatMessageEvent(msg, engineId)); return; case AVAILABILITY_STATE_CHANGE: LogUtils.logV("Parse availability state change:"); engineId = EngineId.PRESENCE_ENGINE; list.add(PushAvailabilityEvent.createPushEvent(msg, engineId)); return; case START_CONVERSATION: LogUtils.logV("Parse new conversation event:"); engineId = EngineId.PRESENCE_ENGINE; list.add(new PushChatConversationEvent(msg, engineId)); return; case CLOSED_CONVERSATION: LogUtils.logV("Parse closed conversation event:"); engineId = EngineId.PRESENCE_ENGINE; list.add(new PushClosedConversationEvent(msg, engineId)); return; case CONVERSATION_END: break; // API events create push message type case PROFILE_CHANGE: engineId = EngineId.SYNCME_ENGINE; break; case CONTACTS_CHANGE: engineId = EngineId.CONTACT_SYNC_ENGINE; break; case TIMELINE_ACTIVITY_CHANGE: case STATUS_ACTIVITY_CHANGE: engineId = EngineId.ACTIVITIES_ENGINE; break; case FRIENDSHIP_REQUEST_RECEIVED: break; case IDENTITY_CHANGE: engineId = EngineId.IDENTITIES_ENGINE; break; case IDENTITY_NETWORK_CHANGE: engineId = EngineId.IDENTITIES_ENGINE; break; case SYSTEM_NOTIFICATION: LogUtils.logE("SYSTEM_NOTIFICATION push msg:" + msg.mHash); list.add(SystemNotification.createFromHashtable(msg.mHash, engineId)); return; default: } list.add(PushEvent.createPushEvent(msg, engineId)); } } }