- Introduced the DeserializationMap class in order to keep a mapping between request and response classes, and how to de-serialize them

- Fixed an infinite loop that was introduced in the de-serialization of the AccountProperties
- Implementing the GetAccounts wrapper in the sample app
This commit is contained in:
Nelson R. Perez 2018-06-05 23:11:28 -05:00
parent c956ebadc1
commit 7c79c7f5bf
3 changed files with 152 additions and 10 deletions

View file

@ -0,0 +1,86 @@
package cy.agorise.graphenej.api.android;
import com.google.common.reflect.TypeToken;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import cy.agorise.graphenej.AccountOptions;
import cy.agorise.graphenej.AssetAmount;
import cy.agorise.graphenej.Authority;
import cy.agorise.graphenej.Transaction;
import cy.agorise.graphenej.api.calls.GetAccounts;
import cy.agorise.graphenej.api.calls.GetBlock;
import cy.agorise.graphenej.models.AccountProperties;
import cy.agorise.graphenej.models.Block;
import cy.agorise.graphenej.operations.CustomOperation;
import cy.agorise.graphenej.operations.LimitOrderCreateOperation;
import cy.agorise.graphenej.operations.TransferOperation;
/**
* Class used to store a mapping of request class to two important things:
*
* 1- The class to which the corresponding response should be de-serialized to
* 2- An instance of the Gson class, with all required type adapters
*/
public class DeserializationMap {
private final String TAG = this.getClass().getName();
private HashMap<Class, Class> mClassMap = new HashMap<>();
private HashMap<Class, Gson> mGsonMap = new HashMap<>();
public DeserializationMap(){
// GetBlock
mClassMap.put(GetBlock.class, Block.class);
Gson getBlockGson = new GsonBuilder()
.registerTypeAdapter(Transaction.class, new Transaction.TransactionDeserializer())
.registerTypeAdapter(TransferOperation.class, new TransferOperation.TransferDeserializer())
.registerTypeAdapter(LimitOrderCreateOperation.class, new LimitOrderCreateOperation.LimitOrderCreateDeserializer())
.registerTypeAdapter(CustomOperation.class, new CustomOperation.CustomOperationDeserializer())
.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer())
.create();
mGsonMap.put(GetBlock.class, getBlockGson);
// GetAccounts
Type GetAccountResponse = new TypeToken<List<AccountProperties>>() {}.getType();
mClassMap.put(GetAccounts.class, GetAccountResponse.getClass());
mClassMap.put(GetAccounts.class, List.class);
Gson getAccountsGson = new GsonBuilder()
.setExclusionStrategies(new GetAccountsExclusionStrategy())
.registerTypeAdapter(Authority.class, new Authority.AuthorityDeserializer())
.registerTypeAdapter(AccountOptions.class, new AccountOptions.AccountOptionsDeserializer())
.create();
mGsonMap.put(GetAccounts.class, getAccountsGson);
}
public Class getReceivedClass(Class _class){
return mClassMap.get(_class);
}
public Gson getGson(Class aClass) {
return mGsonMap.get(aClass);
}
/**
* This class is required in order to break a recursion loop when de-serializing the
* AccountProperties class instance.
*/
private class GetAccountsExclusionStrategy implements ExclusionStrategy {
@Override
public boolean shouldSkipField(FieldAttributes f) {
return false;
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
return clazz == AccountOptions.class;
}
}
}

View file

@ -15,13 +15,17 @@ import java.io.Serializable;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import cy.agorise.graphenej.RPC; import cy.agorise.graphenej.RPC;
import cy.agorise.graphenej.api.ApiAccess; import cy.agorise.graphenej.api.ApiAccess;
import cy.agorise.graphenej.api.ConnectionStatusUpdate; import cy.agorise.graphenej.api.ConnectionStatusUpdate;
import cy.agorise.graphenej.api.bitshares.Nodes; import cy.agorise.graphenej.api.bitshares.Nodes;
import cy.agorise.graphenej.api.calls.ApiCallable; import cy.agorise.graphenej.api.calls.ApiCallable;
import cy.agorise.graphenej.api.calls.GetAccounts;
import cy.agorise.graphenej.models.AccountProperties;
import cy.agorise.graphenej.models.ApiCall; import cy.agorise.graphenej.models.ApiCall;
import cy.agorise.graphenej.models.Block;
import cy.agorise.graphenej.models.JsonRpcResponse; import cy.agorise.graphenej.models.JsonRpcResponse;
import io.reactivex.annotations.Nullable; import io.reactivex.annotations.Nullable;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
@ -68,7 +72,7 @@ public class NetworkService extends Service {
private boolean isLoggedIn = false; private boolean isLoggedIn = false;
private String mLastCall; private String mLastCall;
private int mCurrentId = 0; private long mCurrentId = 0;
// Requested APIs passed to this service // Requested APIs passed to this service
private int mRequestedApis; private int mRequestedApis;
@ -80,6 +84,16 @@ public class NetworkService extends Service {
private Gson gson = new Gson(); private Gson gson = new Gson();
// Map used to keep track of outgoing request ids and its request types. This is just
// one of two required mappings. The second one is implemented by the DeserializationMap
// class.
private HashMap<Long, Class> mRequestClassMap = new HashMap<>();
// This class is used to keep track of the mapping between request classes and response
// payload classes. It also provides a handy method that returns a Gson deserializer instance
// suited for every response type.
private DeserializationMap mDeserializationMap = new DeserializationMap();
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
@ -117,7 +131,7 @@ public class NetworkService extends Service {
client.newWebSocket(request, mWebSocketListener); client.newWebSocket(request, mWebSocketListener);
} }
public int sendMessage(String message){ public long sendMessage(String message){
if(mWebSocket != null){ if(mWebSocket != null){
if(mWebSocket.send(message)){ if(mWebSocket.send(message)){
Log.v(TAG,"-> " + message); Log.v(TAG,"-> " + message);
@ -128,12 +142,13 @@ public class NetworkService extends Service {
return mCurrentId; return mCurrentId;
} }
public int sendMessage(ApiCallable apiCallable, int requiredApi){ public long sendMessage(ApiCallable apiCallable, int requiredApi){
int apiId = 0; int apiId = 0;
if(requiredApi != -1 && mApiIds.containsKey(requiredApi)){ if(requiredApi != -1 && mApiIds.containsKey(requiredApi)){
apiId = mApiIds.get(requiredApi); apiId = mApiIds.get(requiredApi);
} }
ApiCall call = apiCallable.toApiCall(apiId, ++mCurrentId); ApiCall call = apiCallable.toApiCall(apiId, ++mCurrentId);
mRequestClassMap.put(mCurrentId, apiCallable.getClass());
if(mWebSocket.send(call.toJsonString())){ if(mWebSocket.send(call.toJsonString())){
Log.v(TAG,"-> "+call.toJsonString()); Log.v(TAG,"-> "+call.toJsonString());
} }
@ -156,14 +171,12 @@ public class NetworkService extends Service {
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG,"onStartCommand");
return super.onStartCommand(intent, flags, startId); return super.onStartCommand(intent, flags, startId);
} }
@Nullable @Nullable
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
Log.d(TAG,"onBind");
return mBinder; return mBinder;
} }
@ -183,7 +196,6 @@ public class NetworkService extends Service {
@Override @Override
public void onOpen(WebSocket webSocket, Response response) { public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response); super.onOpen(webSocket, response);
Log.d(TAG,"onOpen");
mWebSocket = webSocket; mWebSocket = webSocket;
// Notifying all listeners about the new connection status // Notifying all listeners about the new connection status
@ -247,7 +259,46 @@ public class NetworkService extends Service {
Log.w(TAG,"Error.Msg: "+response.error.message); Log.w(TAG,"Error.Msg: "+response.error.message);
} }
RxBus.getBusInstance().send(response); JsonRpcResponse parsedResponse = null;
Class requestClass = mRequestClassMap.get(response.id);
if(requestClass != null){
// Removing the class entry in the map
mRequestClassMap.remove(mCurrentId);
// Obtaining the response payload class
Class responsePayloadClass = mDeserializationMap.getReceivedClass(requestClass);
Gson gson = mDeserializationMap.getGson(requestClass);
if(responsePayloadClass == Block.class){
// If the response payload is a simple Block instance, we proceed to de-serialize it
Type GetBlockResponse = new TypeToken<JsonRpcResponse<Block>>() {}.getType();
JsonRpcResponse<Block> blockResponse = (JsonRpcResponse) gson.fromJson(text, GetBlockResponse);
parsedResponse = blockResponse;
}else if(responsePayloadClass == List.class){
// If the response payload is a List, further inquiry is required in order to
// determine a list of what is expected here
if(requestClass == GetAccounts.class){
// If the request call was the wrapper to the get_accounts API call, we know
// the response should be in the form of a JsonRpcResponse<List<AccountProperties>>
// so we proceed with that
Type GetAccountsResponse = new TypeToken<JsonRpcResponse<List<AccountProperties>>>(){}.getType();
JsonRpcResponse<List<AccountProperties>> accountResponse = (JsonRpcResponse) gson.fromJson(text, GetAccountsResponse);
parsedResponse = accountResponse;
}else{
Log.w(TAG,"Unknown request class");
}
}else{
Log.w(TAG,"Unhandled situation");
}
}
// In case the parsedResponse instance is null, we fall back to the raw response
if(parsedResponse == null){
parsedResponse = response;
}
// Broadcasting the parsed response to all interested listeners
RxBus.getBusInstance().send(parsedResponse);
} }
private void checkNextRequestedApiAccess(){ private void checkNextRequestedApiAccess(){

View file

@ -45,7 +45,7 @@ public class MainActivity extends AppCompatActivity {
private Disposable mDisposable; private Disposable mDisposable;
private HashMap<Integer, Integer> responseMap = new HashMap<>(); private HashMap<Long, Integer> responseMap = new HashMap<>();
private final int GET_BLOCK_RESPONSE = 0; private final int GET_BLOCK_RESPONSE = 0;
private final int GET_ACCOUNTS_RESPONSE = 1; private final int GET_ACCOUNTS_RESPONSE = 1;
@ -66,6 +66,7 @@ public class MainActivity extends AppCompatActivity {
if(message instanceof String){ if(message instanceof String){
Log.d(TAG,"Got text message: "+(message)); Log.d(TAG,"Got text message: "+(message));
mResponse.setText(mResponse.getText() + ((String) message) + "\n"); mResponse.setText(mResponse.getText() + ((String) message) + "\n");
handleTextMessage((String) message);
}else if(message instanceof ConnectionStatusUpdate){ }else if(message instanceof ConnectionStatusUpdate){
Log.d(TAG,"Got connection update. Status: "+((ConnectionStatusUpdate)message).getConnectionStatus()); Log.d(TAG,"Got connection update. Status: "+((ConnectionStatusUpdate)message).getConnectionStatus());
mConnectionStatus.setText(((ConnectionStatusUpdate) message).getConnectionStatus()); mConnectionStatus.setText(((ConnectionStatusUpdate) message).getConnectionStatus());
@ -76,6 +77,10 @@ public class MainActivity extends AppCompatActivity {
}); });
} }
private void handleTextMessage(String text){
}
/** /**
* Internal method that will decide what to do with each JSON-RPC response * Internal method that will decide what to do with each JSON-RPC response
* *
@ -106,7 +111,7 @@ public class MainActivity extends AppCompatActivity {
@OnClick(R.id.call_get_block) @OnClick(R.id.call_get_block)
public void onGetBlock(View v){ public void onGetBlock(View v){
GetBlock getBlock = new GetBlock(1000000); GetBlock getBlock = new GetBlock(1000000);
int id = mService.sendMessage(getBlock, GetBlock.REQUIRED_API); long id = mService.sendMessage(getBlock, GetBlock.REQUIRED_API);
// Registering the used sequence id // Registering the used sequence id
responseMap.put(id, GET_BLOCK_RESPONSE); responseMap.put(id, GET_BLOCK_RESPONSE);
} }
@ -114,7 +119,7 @@ public class MainActivity extends AppCompatActivity {
@OnClick(R.id.call_get_accounts) @OnClick(R.id.call_get_accounts)
public void onGetAccounts(View v){ public void onGetAccounts(View v){
GetAccounts getAccounts = new GetAccounts(new UserAccount("1.2.1000")); GetAccounts getAccounts = new GetAccounts(new UserAccount("1.2.1000"));
int id = mService.sendMessage(getAccounts, GetBlock.REQUIRED_API); long id = mService.sendMessage(getAccounts, GetBlock.REQUIRED_API);
// Registering the used sequence id // Registering the used sequence id
responseMap.put(id, GET_ACCOUNTS_RESPONSE); responseMap.put(id, GET_ACCOUNTS_RESPONSE);
} }