/* * Copyright (C) 2016 Google Inc. * * 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.rpc; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; import com.googlecode.android_scripting.facade.AndroidFacade; import com.googlecode.android_scripting.jsonrpc.RpcReceiver; import com.googlecode.android_scripting.jsonrpc.RpcReceiverManager; import com.googlecode.android_scripting.util.VisibleForTesting; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; /** * An adapter that wraps {@code Method}. * */ public final class MethodDescriptor { private static final Map, Converter> sConverters = populateConverters(); private final Method mMethod; private final Class mClass; public MethodDescriptor(Class clazz, Method method) { mClass = clazz; mMethod = method; } @Override public String toString() { return mMethod.getDeclaringClass().getCanonicalName() + "." + mMethod.getName(); } /** Collects all methods with {@code RPC} annotation from given class. */ public static Collection collectFrom(Class clazz) { List descriptors = new ArrayList(); for (Method method : clazz.getMethods()) { if (method.isAnnotationPresent(Rpc.class)) { descriptors.add(new MethodDescriptor(clazz, method)); } } return descriptors; } /** * Invokes the call that belongs to this object with the given parameters. Wraps the response * (possibly an exception) in a JSONObject. * * @param parameters * {@code JSONArray} containing the parameters * @return result * @throws Throwable */ public Object invoke(RpcReceiverManager manager, final JSONArray parameters) throws Throwable { final Type[] parameterTypes = getGenericParameterTypes(); final Object[] args = new Object[parameterTypes.length]; final Annotation annotations[][] = getParameterAnnotations(); if (parameters.length() > args.length) { throw new RpcError("Too many parameters specified."); } for (int i = 0; i < args.length; i++) { final Type parameterType = parameterTypes[i]; if (i < parameters.length()) { args[i] = convertParameter(parameters, i, parameterType); } else if (MethodDescriptor.hasDefaultValue(annotations[i])) { args[i] = MethodDescriptor.getDefaultValue(parameterType, annotations[i]); } else { throw new RpcError("Argument " + (i + 1) + " is not present"); } } return invoke(manager, args); } /** * Invokes the call that belongs to this object with the given parameters. Wraps the response * (possibly an exception) in a JSONObject. * * @param parameters {@code Bundle} containing the parameters * @return result * @throws Throwable */ public Object invoke(RpcReceiverManager manager, final Bundle parameters) throws Throwable { final Annotation annotations[][] = getParameterAnnotations(); final Class[] parameterTypes = getMethod().getParameterTypes(); final Object[] args = new Object[parameterTypes.length]; for (int i = 0; i < parameterTypes.length; i++) { Class parameterType = parameterTypes[i]; String parameterName = getName(annotations[i]); if (i < parameterTypes.length) { args[i] = convertParameter(parameters, parameterType, parameterName); } else if (MethodDescriptor.hasDefaultValue(annotations[i])) { args[i] = MethodDescriptor.getDefaultValue(parameterType, annotations[i]); } else { throw new RpcError("Argument " + (i + 1) + " is not present"); } } return invoke(manager, args); } private Object invoke(RpcReceiverManager manager, Object[] args) throws Throwable{ Object result = null; try { result = manager.invoke(mClass, mMethod, args); } catch (Throwable t) { throw t.getCause(); } return result; } /** * Converts a parameter from JSON into a Java Object. * * @return TODO */ // TODO(damonkohler): This signature is a bit weird (auto-refactored). The obvious alternative // would be to work on one supplied parameter and return the converted parameter. However, that's // problematic because you lose the ability to call the getXXX methods on the JSON array. @VisibleForTesting static Object convertParameter(final JSONArray parameters, int index, Type type) throws JSONException, RpcError { try { // Log.d("sl4a", parameters.toString()); // Log.d("sl4a", type.toString()); // We must handle null and numbers explicitly because we cannot magically cast them. We // also need to convert implicitly from numbers to bools. if (parameters.isNull(index)) { return null; } else if (type == Boolean.class) { try { return parameters.getBoolean(index); } catch (JSONException e) { return new Boolean(parameters.getInt(index) != 0); } } else if (type == Long.class) { return parameters.getLong(index); } else if (type == Double.class) { return parameters.getDouble(index); } else if (type == Integer.class) { return parameters.getInt(index); } else if (type == Intent.class) { return buildIntent(parameters.getJSONObject(index)); } else if (type == Integer[].class) { JSONArray list = parameters.getJSONArray(index); Integer[] result = new Integer[list.length()]; for (int i = 0; i < list.length(); i++) { result[i] = list.getInt(i); } return result; } else if (type == byte[].class) { JSONArray list = parameters.getJSONArray(index); byte[] result = new byte[list.length()]; for (int i = 0; i < list.length(); i++) { result[i] = (byte)list.getInt(i); } return result; } else if (type == String[].class) { JSONArray list = parameters.getJSONArray(index); String[] result = new String[list.length()]; for (int i = 0; i < list.length(); i++) { result[i] = list.getString(i); } return result; } else if (type == JSONObject.class) { return parameters.getJSONObject(index); } else { // Magically cast the parameter to the right Java type. return ((Class) type).cast(parameters.get(index)); } } catch (ClassCastException e) { throw new RpcError("Argument " + (index + 1) + " should be of type " + ((Class) type).getSimpleName() + "."); } } private Object convertParameter(Bundle bundle, Class type, String name) { Object param = null; if (type.isAssignableFrom(Boolean.class)) { param = bundle.getBoolean(name, false); } if (type.isAssignableFrom(Boolean[].class)) { param = bundle.getBooleanArray(name); } if (type.isAssignableFrom(String.class)) { param = bundle.getString(name); } if (type.isAssignableFrom(String[].class)) { param = bundle.getStringArray(name); } if (type.isAssignableFrom(Integer.class)) { param = bundle.getInt(name, 0); } if (type.isAssignableFrom(Integer[].class)) { param = bundle.getIntArray(name); } if (type.isAssignableFrom(Bundle.class)) { param = bundle.getBundle(name); } if (type.isAssignableFrom(Parcelable.class)) { param = bundle.getParcelable(name); } if (type.isAssignableFrom(Parcelable[].class)) { param = bundle.getParcelableArray(name); } if (type.isAssignableFrom(Intent.class)) { param = bundle.getParcelable(name); } return param; } public static Object buildIntent(JSONObject jsonObject) throws JSONException { Intent intent = new Intent(); if (jsonObject.has("action")) { intent.setAction(jsonObject.getString("action")); } if (jsonObject.has("data") && jsonObject.has("type")) { intent.setDataAndType(Uri.parse(jsonObject.optString("data", null)), jsonObject.optString("type", null)); } else if (jsonObject.has("data")) { intent.setData(Uri.parse(jsonObject.optString("data", null))); } else if (jsonObject.has("type")) { intent.setType(jsonObject.optString("type", null)); } if (jsonObject.has("packagename") && jsonObject.has("classname")) { intent.setClassName(jsonObject.getString("packagename"), jsonObject.getString("classname")); } if (jsonObject.has("flags")) { intent.setFlags(jsonObject.getInt("flags")); } if (!jsonObject.isNull("extras")) { AndroidFacade.putExtrasFromJsonObject(jsonObject.getJSONObject("extras"), intent); } if (!jsonObject.isNull("categories")) { JSONArray categories = jsonObject.getJSONArray("categories"); for (int i = 0; i < categories.length(); i++) { intent.addCategory(categories.getString(i)); } } return intent; } public Method getMethod() { return mMethod; } public Class getDeclaringClass() { return mClass; } public String getName() { if (mMethod.isAnnotationPresent(RpcName.class)) { return mMethod.getAnnotation(RpcName.class).name(); } return mMethod.getName(); } public Type[] getGenericParameterTypes() { return mMethod.getGenericParameterTypes(); } public Annotation[][] getParameterAnnotations() { return mMethod.getParameterAnnotations(); } /** * Returns a human-readable help text for this RPC, based on annotations in the source code. * * @return derived help string */ public String getHelp() { StringBuilder helpBuilder = new StringBuilder(); Rpc rpcAnnotation = mMethod.getAnnotation(Rpc.class); helpBuilder.append(mMethod.getName()); helpBuilder.append("("); final Class[] parameterTypes = mMethod.getParameterTypes(); final Type[] genericParameterTypes = mMethod.getGenericParameterTypes(); final Annotation[][] annotations = mMethod.getParameterAnnotations(); for (int i = 0; i < parameterTypes.length; i++) { if (i == 0) { helpBuilder.append("\n "); } else { helpBuilder.append(",\n "); } helpBuilder.append(getHelpForParameter(genericParameterTypes[i], annotations[i])); } helpBuilder.append(")\n\n"); helpBuilder.append(rpcAnnotation.description()); if (!rpcAnnotation.returns().equals("")) { helpBuilder.append("\n"); helpBuilder.append("\nReturns:\n "); helpBuilder.append(rpcAnnotation.returns()); } if (mMethod.isAnnotationPresent(RpcStartEvent.class)) { String eventName = mMethod.getAnnotation(RpcStartEvent.class).value(); helpBuilder.append(String.format("\n\nGenerates \"%s\" events.", eventName)); } if (mMethod.isAnnotationPresent(RpcDeprecated.class)) { String replacedBy = mMethod.getAnnotation(RpcDeprecated.class).value(); String release = mMethod.getAnnotation(RpcDeprecated.class).release(); helpBuilder.append(String.format("\n\nDeprecated in %s! Please use %s instead.", release, replacedBy)); } return helpBuilder.toString(); } /** * Returns the help string for one particular parameter. This respects optional parameters. * * @param parameterType * (generic) type of the parameter * @param annotations * annotations of the parameter, may be null * @return string describing the parameter based on source code annotations */ private static String getHelpForParameter(Type parameterType, Annotation[] annotations) { StringBuilder result = new StringBuilder(); appendTypeName(result, parameterType); result.append(" "); result.append(getName(annotations)); if (hasDefaultValue(annotations)) { result.append("[optional"); if (hasExplicitDefaultValue(annotations)) { result.append(", default " + getDefaultValue(parameterType, annotations)); } result.append("]"); } String description = getDescription(annotations); if (description.length() > 0) { result.append(": "); result.append(description); } return result.toString(); } /** * Appends the name of the given type to the {@link StringBuilder}. * * @param builder * string builder to append to * @param type * type whose name to append */ private static void appendTypeName(final StringBuilder builder, final Type type) { if (type instanceof Class) { builder.append(((Class) type).getSimpleName()); } else { ParameterizedType parametrizedType = (ParameterizedType) type; builder.append(((Class) parametrizedType.getRawType()).getSimpleName()); builder.append("<"); Type[] arguments = parametrizedType.getActualTypeArguments(); for (int i = 0; i < arguments.length; i++) { if (i > 0) { builder.append(", "); } appendTypeName(builder, arguments[i]); } builder.append(">"); } } /** * Returns parameter descriptors suitable for the RPC call text representation. * *

* Uses parameter value, default value or name, whatever is available first. * * @return an array of parameter descriptors */ public ParameterDescriptor[] getParameterValues(String[] values) { Type[] parameterTypes = mMethod.getGenericParameterTypes(); Annotation[][] parametersAnnotations = mMethod.getParameterAnnotations(); ParameterDescriptor[] parameters = new ParameterDescriptor[parametersAnnotations.length]; for (int index = 0; index < parameters.length; index++) { String value; if (index < values.length) { value = values[index]; } else if (hasDefaultValue(parametersAnnotations[index])) { Object defaultValue = getDefaultValue(parameterTypes[index], parametersAnnotations[index]); if (defaultValue == null) { value = null; } else { value = String.valueOf(defaultValue); } } else { value = getName(parametersAnnotations[index]); } parameters[index] = new ParameterDescriptor(value, parameterTypes[index]); } return parameters; } /** * Returns parameter hints. * * @return an array of parameter hints */ public String[] getParameterHints() { Annotation[][] parametersAnnotations = mMethod.getParameterAnnotations(); String[] hints = new String[parametersAnnotations.length]; for (int index = 0; index < hints.length; index++) { String name = getName(parametersAnnotations[index]); String description = getDescription(parametersAnnotations[index]); String hint = "No paramenter description."; if (!name.equals("") && !description.equals("")) { hint = name + ": " + description; } else if (!name.equals("")) { hint = name; } else if (!description.equals("")) { hint = description; } hints[index] = hint; } return hints; } /** * Extracts the formal parameter name from an annotation. * * @param annotations * the annotations of the parameter * @return the formal name of the parameter */ private static String getName(Annotation[] annotations) { for (Annotation a : annotations) { if (a instanceof RpcParameter) { return ((RpcParameter) a).name(); } } throw new IllegalStateException("No parameter name"); } /** * Extracts the parameter description from its annotations. * * @param annotations * the annotations of the parameter * @return the description of the parameter */ private static String getDescription(Annotation[] annotations) { for (Annotation a : annotations) { if (a instanceof RpcParameter) { return ((RpcParameter) a).description(); } } throw new IllegalStateException("No parameter description"); } /** * Returns the default value for a specific parameter. * * @param parameterType * parameterType * @param annotations * annotations of the parameter */ public static Object getDefaultValue(Type parameterType, Annotation[] annotations) { for (Annotation a : annotations) { if (a instanceof RpcDefault) { RpcDefault defaultAnnotation = (RpcDefault) a; Converter converter = converterFor(parameterType, defaultAnnotation.converter()); return converter.convert(defaultAnnotation.value()); } else if (a instanceof RpcOptional) { return null; } } throw new IllegalStateException("No default value for " + parameterType); } @SuppressWarnings("rawtypes") private static Converter converterFor(Type parameterType, Class converterClass) { if (converterClass == Converter.class) { Converter converter = sConverters.get(parameterType); if (converter == null) { throw new IllegalArgumentException("No predefined converter found for " + parameterType); } return converter; } try { Constructor constructor = converterClass.getConstructor(new Class[0]); return (Converter) constructor.newInstance(new Object[0]); } catch (Exception e) { throw new IllegalArgumentException("Cannot create converter from " + converterClass.getCanonicalName()); } } /** * Determines whether or not this parameter has default value. * * @param annotations * annotations of the parameter */ public static boolean hasDefaultValue(Annotation[] annotations) { for (Annotation a : annotations) { if (a instanceof RpcDefault || a instanceof RpcOptional) { return true; } } return false; } /** * Returns whether the default value is specified for a specific parameter. * * @param annotations * annotations of the parameter */ @VisibleForTesting static boolean hasExplicitDefaultValue(Annotation[] annotations) { for (Annotation a : annotations) { if (a instanceof RpcDefault) { return true; } } return false; } /** Returns the converters for {@code String}, {@code Integer} and {@code Boolean}. */ private static Map, Converter> populateConverters() { Map, Converter> converters = new HashMap, Converter>(); converters.put(String.class, new Converter() { @Override public String convert(String value) { return value; } }); converters.put(Integer.class, new Converter() { @Override public Integer convert(String input) { try { return Integer.decode(input); } catch (NumberFormatException e) { throw new IllegalArgumentException("'" + input + "' is not an integer"); } } }); converters.put(Boolean.class, new Converter() { @Override public Boolean convert(String input) { if (input == null) { return null; } input = input.toLowerCase(); if (input.equals("true")) { return Boolean.TRUE; } if (input.equals("false")) { return Boolean.FALSE; } throw new IllegalArgumentException("'" + input + "' is not a boolean"); } }); return converters; } }