/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.stetho.inspector;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import com.facebook.stetho.common.ExceptionUtil;
import com.facebook.stetho.common.Util;
import com.facebook.stetho.inspector.jsonrpc.JsonRpcException;
import com.facebook.stetho.inspector.jsonrpc.JsonRpcPeer;
import com.facebook.stetho.inspector.jsonrpc.JsonRpcResult;
import com.facebook.stetho.inspector.jsonrpc.protocol.EmptyResult;
import com.facebook.stetho.inspector.jsonrpc.protocol.JsonRpcError;
import com.facebook.stetho.inspector.protocol.ChromeDevtoolsDomain;
import com.facebook.stetho.inspector.protocol.ChromeDevtoolsMethod;
import com.facebook.stetho.json.ObjectMapper;
import org.json.JSONException;
import org.json.JSONObject;
@ThreadSafe
public class MethodDispatcher {
@GuardedBy("this")
private Map<String, MethodDispatchHelper> mMethods;
private final ObjectMapper mObjectMapper;
private final Iterable<ChromeDevtoolsDomain> mDomainHandlers;
public MethodDispatcher(
ObjectMapper objectMapper,
Iterable<ChromeDevtoolsDomain> domainHandlers) {
mObjectMapper = objectMapper;
mDomainHandlers = domainHandlers;
}
private synchronized MethodDispatchHelper findMethodDispatcher(String methodName) {
if (mMethods == null) {
mMethods = buildDispatchTable(mObjectMapper, mDomainHandlers);
}
return mMethods.get(methodName);
}
public JSONObject dispatch(JsonRpcPeer peer, String methodName, @Nullable JSONObject params)
throws JsonRpcException {
MethodDispatchHelper dispatchHelper = findMethodDispatcher(methodName);
if (dispatchHelper == null) {
throw new JsonRpcException(new JsonRpcError(JsonRpcError.ErrorCode.METHOD_NOT_FOUND,
"Not implemented: " + methodName,
null /* data */));
}
try {
return dispatchHelper.invoke(peer, params);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
ExceptionUtil.propagateIfInstanceOf(cause, JsonRpcException.class);
throw ExceptionUtil.propagate(cause);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (JSONException e) {
throw new JsonRpcException(new JsonRpcError(JsonRpcError.ErrorCode.INTERNAL_ERROR,
e.toString(),
null /* data */));
}
}
private static class MethodDispatchHelper {
private final ObjectMapper mObjectMapper;
private final ChromeDevtoolsDomain mInstance;
private final Method mMethod;
public MethodDispatchHelper(ObjectMapper objectMapper,
ChromeDevtoolsDomain instance,
Method method) {
mObjectMapper = objectMapper;
mInstance = instance;
mMethod = method;
}
public JSONObject invoke(JsonRpcPeer peer, @Nullable JSONObject params)
throws InvocationTargetException, IllegalAccessException, JSONException, JsonRpcException {
Object internalResult = mMethod.invoke(mInstance, peer, params);
if (internalResult == null || internalResult instanceof EmptyResult) {
return new JSONObject();
} else {
JsonRpcResult convertableResult = (JsonRpcResult)internalResult;
return mObjectMapper.convertValue(convertableResult, JSONObject.class);
}
}
}
private static Map<String, MethodDispatchHelper> buildDispatchTable(
ObjectMapper objectMapper,
Iterable<ChromeDevtoolsDomain> domainHandlers) {
Util.throwIfNull(objectMapper);
HashMap<String, MethodDispatchHelper> methods = new HashMap<String, MethodDispatchHelper>();
for (ChromeDevtoolsDomain domainHandler : Util.throwIfNull(domainHandlers)) {
Class<?> handlerClass = domainHandler.getClass();
String domainName = handlerClass.getSimpleName();
for (Method method : handlerClass.getDeclaredMethods()) {
if (isDevtoolsMethod(method)) {
MethodDispatchHelper dispatchHelper = new MethodDispatchHelper(
objectMapper,
domainHandler,
method);
methods.put(domainName + "." + method.getName(), dispatchHelper);
}
}
}
return Collections.unmodifiableMap(methods);
}
/**
* Determines if the method is a {@link ChromeDevtoolsMethod}, and validates accordingly
* if it is.
*
* @throws IllegalArgumentException Thrown if it is a {@link ChromeDevtoolsMethod} but
* it otherwise fails to satisfy requirements.
*/
private static boolean isDevtoolsMethod(Method method) throws IllegalArgumentException {
if (!method.isAnnotationPresent(ChromeDevtoolsMethod.class)) {
return false;
} else {
Class<?> args[] = method.getParameterTypes();
String methodName = method.getDeclaringClass().getSimpleName() + "." + method.getName();
Util.throwIfNot(args.length == 2,
"%s: expected 2 args, got %s",
methodName,
args.length);
Util.throwIfNot(args[0].equals(JsonRpcPeer.class),
"%s: expected 1st arg of JsonRpcPeer, got %s",
methodName,
args[0].getName());
Util.throwIfNot(args[1].equals(JSONObject.class),
"%s: expected 2nd arg of JSONObject, got %s",
methodName,
args[1].getName());
Class<?> returnType = method.getReturnType();
if (!returnType.equals(void.class)) {
Util.throwIfNot(JsonRpcResult.class.isAssignableFrom(returnType),
"%s: expected JsonRpcResult return type, got %s",
methodName,
returnType.getName());
}
return true;
}
}
}