blob: 8f8d887d1731840034bb6a8e907d3f1acb29a2f3 [file] [log] [blame]
/*
* Copyright (C) 2018 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.googlecode.android_scripting.jsonrpc;
import com.googlecode.android_scripting.Log;
import com.googlecode.android_scripting.Sl4aErrors;
import com.googlecode.android_scripting.rpc.MethodDescriptor;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* A class that parses given JSON RPC Messages, and handles their execution.
*/
public class JsonRpcHandler {
private int mNextSessionId = 1;
private String mSessionId = "";
private final RpcReceiverManagerFactory mManagerFactory;
public JsonRpcHandler(RpcReceiverManagerFactory managerFactory) {
mManagerFactory = managerFactory;
}
/**
* Returns the response object for the given request.
* <p>
* The response will be returned as a JSONObject, unless something fatal has occurred. In that
* case, the resulting object will be a JSON formatted string.
*
* @param request the received request as a string
* @return the response to the request
*/
public Object getResponse(String request) {
try {
return getResponse(new JSONObject(request));
} catch (JSONException ignored) {
// May have been a bad request.
}
try {
return Sl4aErrors.JSON_RPC_REQUEST_NOT_JSON.toJson(JSONObject.NULL);
} catch (JSONException e) {
// This error will never occur.
Log.e("Received JSONException on invalid request.", e);
return JsonRpcResult.wtf();
}
}
/**
* Returns the response for the given request.
* <p>
* This message will be returned as a JSONObject, unless something fatal has occurred. In that
* case, the resulting object will be a JSON formatted string.
*
* @param request the JSON-RPC request message as a JSONObject
* @return the response to the request
*/
public Object getResponse(JSONObject request) {
if (isSessionManagementRpc(request)) {
return handleSessionRpc(request);
}
Object validationErrors = getValidationErrors(request);
if (validationErrors != null) {
return validationErrors;
}
try {
Object id = request.get("id");
String method = request.getString("method");
JSONArray params = new JSONArray();
if (request.has("params")) {
params = request.getJSONArray("params");
}
RpcReceiverManager receiverManager =
mManagerFactory.getRpcReceiverManagers().get(mSessionId);
if (receiverManager == null) {
Log.e("Unable to find sessionId \"" + mSessionId + "\".");
for (String key : mManagerFactory.getRpcReceiverManagers().keySet()) {
Log.d("Available key: " + key);
}
return Sl4aErrors.SESSION_INVALID.toJson(id, mSessionId);
}
MethodDescriptor rpc = receiverManager.getMethodDescriptor(method);
if (rpc == null) {
return Sl4aErrors.JSON_RPC_UNKNOWN_RPC_METHOD.toJson(id, method);
}
Object rpcResult;
try {
rpcResult = rpc.invoke(receiverManager, params);
} catch (Throwable t) {
Log.e("RPC call threw an error.", t);
return Sl4aErrors.JSON_RPC_FACADE_EXCEPTION_OCCURRED.toJson(id, t);
}
try {
return JsonRpcResult.result(id, rpcResult);
} catch (JSONException e) {
// This error is rare, but may occur when the JsonBuilder fails to properly
// convert the resulting class from the RPC into valid JSON. This may happen
// when a resulting number is returned as NaN or infinite, but is not converted
// to a String first.
Log.e("Unable to build object of class \"" + rpcResult.getClass() + "\".", e);
return Sl4aErrors.JSON_RPC_RESULT_NOT_JSON_VALID.toJson(id, e);
} catch (Throwable t) {
// This error will occur whenever the underlying Android APIs built against are
// unavailable for the version of Android SL4A is installed onto.
Log.e("Unable to build object of class \"" + rpcResult.getClass() + "\".", t);
return Sl4aErrors.JSON_RPC_FAILED_TO_BUILD_RESULT.toJson(id, t);
}
} catch (JSONException exception) {
// This error should never happen. NULL and all values found within Sl4aErrors are
// guaranteed not to raise an error when being converted to JSON.
// Also, all JSONObject.get() calls are only done after they have been validated to
// exist after a corresponding JSONObject.has() call has been made.
// Returning a raw string here to prevent the need to catch JsonRpcResult.error().
Log.e(exception);
return JsonRpcResult.wtf();
}
}
/**
* Returns a validation error if the request is invalid. Null otherwise.
* @param message the request message
* @return a JSONObject error response, or null if the message is valid
*/
private Object getValidationErrors(JSONObject message) {
Object id = JSONObject.NULL;
try {
if (!message.has("id")) {
return Sl4aErrors.JSON_RPC_MISSING_ID.toJson(id);
} else {
id = message.get("id");
}
if (!message.has("method")) {
return Sl4aErrors.JSON_RPC_MISSING_METHOD.toJson(id);
} else if (!(message.get("method") instanceof String)) {
return Sl4aErrors.JSON_RPC_METHOD_NOT_STRING.toJson(id);
}
if (message.has("params") && !(message.get("params") instanceof JSONArray)) {
return Sl4aErrors.JSON_RPC_PARAMS_NOT_AN_ARRAY.toJson(id);
}
} catch (JSONException exception) {
// This error should never happen. NULL and all values found within Sl4aException are
// guaranteed not to raise an error when being converted to JSON.
// Also, all JSONObject.get() calls are only done after they have been validated to
// exist after a corresponding JSONObject.has() call has been made.
// Returning a raw string here to prevent the need to wrap JsonRpcResult.error().
Log.e(exception);
return JsonRpcResult.wtf();
}
return null;
}
//TODO(markdr): Refactor this function into a class that handles session management.
private boolean isSessionManagementRpc(JSONObject request) {
return request.has("cmd") && request.has("uid");
}
//TODO(markdr): Refactor this function into a class that handles session management.
private Object handleSessionRpc(JSONObject request) {
try {
Object id = request.get("uid");
String cmd = request.getString("cmd");
switch (cmd) {
case "initiate": {
String newId;
synchronized (mManagerFactory) {
newId = "" + mNextSessionId;
while (mManagerFactory.getRpcReceiverManagers().containsKey(newId)) {
newId = "" + mNextSessionId++;
}
mSessionId = newId;
mManagerFactory.create(newId);
}
return "{\"uid\": \"" + newId + "\", \"status\":true}";
}
case "continue": {
for (String key : mManagerFactory.getRpcReceiverManagers().keySet()) {
Log.d("Available key: " + key);
}
String stringId = id.toString();
boolean canContinue =
mManagerFactory.getRpcReceiverManagers().containsKey(stringId);
if (canContinue) {
mSessionId = stringId;
return "{\"uid\":\"" + id + "\",\"status\":true}";
}
return "{\"uid\":\"" + id + "\",\"status\":false,"
+ "\"error\":\"Session does not exist.\"}";
}
default: {
// This case should never be reached if isSessionManagementRpc is called first.
Log.e("Hit default case for handling session rpcs.");
return JsonRpcResult.wtf();
}
}
} catch (JSONException jsonException) {
// This error will never occur, because the above IDs and objects will always be JSON
// serializable.
Log.e("Exception during handleRpcCall: " + jsonException);
return JsonRpcResult.wtf();
}
}
}