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