Home | History | Annotate | Download | only in rpc
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.googlecode.android_scripting.rpc;
     18 
     19 import android.content.Intent;
     20 import android.net.Uri;
     21 import android.os.Bundle;
     22 import android.os.Parcelable;
     23 
     24 import com.googlecode.android_scripting.facade.AndroidFacade;
     25 import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
     26 import com.googlecode.android_scripting.jsonrpc.RpcReceiverManager;
     27 import com.googlecode.android_scripting.util.VisibleForTesting;
     28 
     29 import java.lang.annotation.Annotation;
     30 import java.lang.reflect.Constructor;
     31 import java.lang.reflect.Method;
     32 import java.lang.reflect.ParameterizedType;
     33 import java.lang.reflect.Type;
     34 import java.util.ArrayList;
     35 import java.util.Collection;
     36 import java.util.HashMap;
     37 import java.util.List;
     38 import java.util.Map;
     39 
     40 import org.json.JSONArray;
     41 import org.json.JSONException;
     42 import org.json.JSONObject;
     43 
     44 /**
     45  * An adapter that wraps {@code Method}.
     46  */
     47 public final class MethodDescriptor {
     48     private static final Map<Class<?>, Converter<?>> sConverters = populateConverters();
     49 
     50     private final Method mMethod;
     51     private final Class<? extends RpcReceiver> mClass;
     52 
     53     public MethodDescriptor(Class<? extends RpcReceiver> clazz, Method method) {
     54         mClass = clazz;
     55         mMethod = method;
     56     }
     57 
     58     @Override
     59     public String toString() {
     60         return mMethod.getDeclaringClass().getCanonicalName() + "." + mMethod.getName();
     61     }
     62 
     63     /** Collects all methods with {@code RPC} annotation from given class. */
     64     public static Collection<MethodDescriptor> collectFrom(Class<? extends RpcReceiver> clazz) {
     65         List<MethodDescriptor> descriptors = new ArrayList<MethodDescriptor>();
     66         for (Method method : clazz.getMethods()) {
     67             if (method.isAnnotationPresent(Rpc.class)) {
     68                 descriptors.add(new MethodDescriptor(clazz, method));
     69             }
     70         }
     71         return descriptors;
     72     }
     73 
     74     /**
     75      * Invokes the call that belongs to this object with the given parameters. Wraps the response
     76      * (possibly an exception) in a JSONObject.
     77      *
     78      * @param parameters {@code JSONArray} containing the parameters
     79      * @return result
     80      * @throws Throwable
     81      */
     82     public Object invoke(RpcReceiverManager manager, final JSONArray parameters) throws Throwable {
     83 
     84         final Type[] parameterTypes = getGenericParameterTypes();
     85         final Object[] args = new Object[parameterTypes.length];
     86         final Annotation annotations[][] = getParameterAnnotations();
     87 
     88         if (parameters.length() > args.length) {
     89             throw new RpcError("Too many parameters specified.");
     90         }
     91 
     92         for (int i = 0; i < args.length; i++) {
     93             final Type parameterType = parameterTypes[i];
     94             if (i < parameters.length()) {
     95                 args[i] = convertParameter(parameters, i, parameterType);
     96             } else if (MethodDescriptor.hasDefaultValue(annotations[i])) {
     97                 args[i] = MethodDescriptor.getDefaultValue(parameterType, annotations[i]);
     98             } else {
     99                 throw new RpcError("Argument " + (i + 1) + " is not present");
    100             }
    101         }
    102 
    103         return invoke(manager, args);
    104     }
    105 
    106     /**
    107      * Invokes the call that belongs to this object with the given parameters. Wraps the response
    108      * (possibly an exception) in a JSONObject.
    109      *
    110      * @param parameters {@code Bundle} containing the parameters
    111      * @return result
    112      * @throws Throwable
    113      */
    114     public Object invoke(RpcReceiverManager manager, final Bundle parameters) throws Throwable {
    115         final Annotation annotations[][] = getParameterAnnotations();
    116         final Class<?>[] parameterTypes = getMethod().getParameterTypes();
    117         final Object[] args = new Object[parameterTypes.length];
    118 
    119         for (int i = 0; i < parameterTypes.length; i++) {
    120             Class<?> parameterType = parameterTypes[i];
    121             String parameterName = getName(annotations[i]);
    122             if (i < parameterTypes.length) {
    123                 args[i] = convertParameter(parameters, parameterType, parameterName);
    124             } else if (MethodDescriptor.hasDefaultValue(annotations[i])) {
    125                 args[i] = MethodDescriptor.getDefaultValue(parameterType, annotations[i]);
    126             } else {
    127                 throw new RpcError("Argument " + (i + 1) + " is not present");
    128             }
    129         }
    130         return invoke(manager, args);
    131     }
    132 
    133     private Object invoke(RpcReceiverManager manager, Object[] args) throws Throwable {
    134         Object result = null;
    135         try {
    136             result = manager.invoke(mClass, mMethod, args);
    137         } catch (Throwable t) {
    138             throw t.getCause();
    139         }
    140         return result;
    141     }
    142 
    143     /**
    144      * Converts a parameter from JSON into a Java Object.
    145      *
    146      * @return TODO
    147      */
    148     // TODO(damonkohler): This signature is a bit weird (auto-refactored). The obvious alternative
    149     // would be to work on one supplied parameter and return the converted parameter. However,
    150     // that's problematic because you lose the ability to call the getXXX methods on the JSON array.
    151     @VisibleForTesting
    152     static Object convertParameter(final JSONArray parameters, int index, Type type)
    153             throws JSONException, RpcError {
    154         try {
    155             // Log.d("sl4a", parameters.toString());
    156             // Log.d("sl4a", type.toString());
    157             // We must handle null and numbers explicitly because we cannot magically cast them. We
    158             // also need to convert implicitly from numbers to bools.
    159             if (parameters.isNull(index)) {
    160                 return null;
    161             } else if (type == Boolean.class) {
    162                 try {
    163                     return parameters.getBoolean(index);
    164                 } catch (JSONException e) {
    165                     return new Boolean(parameters.getInt(index) != 0);
    166                 }
    167             } else if (type == Long.class) {
    168                 return parameters.getLong(index);
    169             } else if (type == Double.class) {
    170                 return parameters.getDouble(index);
    171             } else if (type == Integer.class) {
    172                 return parameters.getInt(index);
    173             } else if (type == Intent.class) {
    174                 return buildIntent(parameters.getJSONObject(index));
    175             } else if (type == Integer[].class) {
    176                 JSONArray list = parameters.getJSONArray(index);
    177                 Integer[] result = new Integer[list.length()];
    178                 for (int i = 0; i < list.length(); i++) {
    179                     result[i] = list.getInt(i);
    180                 }
    181                 return result;
    182             } else if (type == byte[].class) {
    183                 JSONArray list = parameters.getJSONArray(index);
    184                 byte[] result = new byte[list.length()];
    185                 for (int i = 0; i < list.length(); i++) {
    186                     result[i] = (byte) list.getInt(i);
    187                 }
    188                 return result;
    189             } else if (type == String[].class) {
    190                 JSONArray list = parameters.getJSONArray(index);
    191                 String[] result = new String[list.length()];
    192                 for (int i = 0; i < list.length(); i++) {
    193                     result[i] = list.getString(i);
    194                 }
    195                 return result;
    196             } else if (type == JSONObject.class) {
    197                 return parameters.getJSONObject(index);
    198             } else {
    199                 // Magically cast the parameter to the right Java type.
    200                 return ((Class<?>) type).cast(parameters.get(index));
    201             }
    202         } catch (ClassCastException e) {
    203             throw new RpcError("Argument " + (index + 1) + " should be of type "
    204                     + ((Class<?>) type).getSimpleName() + ".");
    205         }
    206     }
    207 
    208     private Object convertParameter(Bundle bundle, Class<?> type, String name) {
    209         Object param = null;
    210         if (type.isAssignableFrom(Boolean.class)) {
    211             param = bundle.getBoolean(name, false);
    212         }
    213         if (type.isAssignableFrom(Boolean[].class)) {
    214             param = bundle.getBooleanArray(name);
    215         }
    216         if (type.isAssignableFrom(String.class)) {
    217             param = bundle.getString(name);
    218         }
    219         if (type.isAssignableFrom(String[].class)) {
    220             param = bundle.getStringArray(name);
    221         }
    222         if (type.isAssignableFrom(Integer.class)) {
    223             param = bundle.getInt(name, 0);
    224         }
    225         if (type.isAssignableFrom(Integer[].class)) {
    226             param = bundle.getIntArray(name);
    227         }
    228         if (type.isAssignableFrom(Bundle.class)) {
    229             param = bundle.getBundle(name);
    230         }
    231         if (type.isAssignableFrom(Parcelable.class)) {
    232             param = bundle.getParcelable(name);
    233         }
    234         if (type.isAssignableFrom(Parcelable[].class)) {
    235             param = bundle.getParcelableArray(name);
    236         }
    237         if (type.isAssignableFrom(Intent.class)) {
    238             param = bundle.getParcelable(name);
    239         }
    240         return param;
    241     }
    242 
    243     public static Object buildIntent(JSONObject jsonObject) throws JSONException {
    244         Intent intent = new Intent();
    245         if (jsonObject.has("action")) {
    246             intent.setAction(jsonObject.getString("action"));
    247         }
    248         if (jsonObject.has("data") && jsonObject.has("type")) {
    249             intent.setDataAndType(Uri.parse(jsonObject.optString("data", null)),
    250                     jsonObject.optString("type", null));
    251         } else if (jsonObject.has("data")) {
    252             intent.setData(Uri.parse(jsonObject.optString("data", null)));
    253         } else if (jsonObject.has("type")) {
    254             intent.setType(jsonObject.optString("type", null));
    255         }
    256         if (jsonObject.has("packagename") && jsonObject.has("classname")) {
    257             intent.setClassName(jsonObject.getString("packagename"),
    258                     jsonObject.getString("classname"));
    259         }
    260         if (jsonObject.has("flags")) {
    261             intent.setFlags(jsonObject.getInt("flags"));
    262         }
    263         if (!jsonObject.isNull("extras")) {
    264             AndroidFacade.putExtrasFromJsonObject(jsonObject.getJSONObject("extras"), intent);
    265         }
    266         if (!jsonObject.isNull("categories")) {
    267             JSONArray categories = jsonObject.getJSONArray("categories");
    268             for (int i = 0; i < categories.length(); i++) {
    269                 intent.addCategory(categories.getString(i));
    270             }
    271         }
    272         return intent;
    273     }
    274 
    275     public Method getMethod() {
    276         return mMethod;
    277     }
    278 
    279     public Class<? extends RpcReceiver> getDeclaringClass() {
    280         return mClass;
    281     }
    282 
    283     public String getName() {
    284         if (mMethod.isAnnotationPresent(RpcName.class)) {
    285             return mMethod.getAnnotation(RpcName.class).name();
    286         }
    287         return mMethod.getName();
    288     }
    289 
    290     public Type[] getGenericParameterTypes() {
    291         return mMethod.getGenericParameterTypes();
    292     }
    293 
    294     public Annotation[][] getParameterAnnotations() {
    295         return mMethod.getParameterAnnotations();
    296     }
    297 
    298     /**
    299      * Returns a human-readable help text for this RPC, based on annotations in the source code.
    300      *
    301      * @return derived help string
    302      */
    303     public String getHelp() {
    304         StringBuilder helpBuilder = new StringBuilder();
    305         Rpc rpcAnnotation = mMethod.getAnnotation(Rpc.class);
    306 
    307         helpBuilder.append(mMethod.getName());
    308         helpBuilder.append("(");
    309         final Class<?>[] parameterTypes = mMethod.getParameterTypes();
    310         final Type[] genericParameterTypes = mMethod.getGenericParameterTypes();
    311         final Annotation[][] annotations = mMethod.getParameterAnnotations();
    312         for (int i = 0; i < parameterTypes.length; i++) {
    313             if (i == 0) {
    314                 helpBuilder.append("\n  ");
    315             } else {
    316                 helpBuilder.append(",\n  ");
    317             }
    318 
    319             helpBuilder.append(getHelpForParameter(genericParameterTypes[i], annotations[i]));
    320         }
    321         helpBuilder.append(")\n\n");
    322         helpBuilder.append(rpcAnnotation.description());
    323         if (!rpcAnnotation.returns().equals("")) {
    324             helpBuilder.append("\n");
    325             helpBuilder.append("\nReturns:\n  ");
    326             helpBuilder.append(rpcAnnotation.returns());
    327         }
    328 
    329         if (mMethod.isAnnotationPresent(RpcStartEvent.class)) {
    330             String eventName = mMethod.getAnnotation(RpcStartEvent.class).value();
    331             helpBuilder.append(String.format("\n\nGenerates \"%s\" events.", eventName));
    332         }
    333 
    334         if (mMethod.isAnnotationPresent(RpcDeprecated.class)) {
    335             String replacedBy = mMethod.getAnnotation(RpcDeprecated.class).value();
    336             String release = mMethod.getAnnotation(RpcDeprecated.class).release();
    337             helpBuilder.append(String.format("\n\nDeprecated in %s! Please use %s instead.",
    338                     release, replacedBy));
    339         }
    340 
    341         return helpBuilder.toString();
    342     }
    343 
    344     /**
    345      * Returns the help string for one particular parameter. This respects optional parameters.
    346      *
    347      * @param parameterType (generic) type of the parameter
    348      * @param annotations   annotations of the parameter, may be null
    349      * @return string describing the parameter based on source code annotations
    350      */
    351     private static String getHelpForParameter(Type parameterType, Annotation[] annotations) {
    352         StringBuilder result = new StringBuilder();
    353 
    354         appendTypeName(result, parameterType);
    355         result.append(" ");
    356         result.append(getName(annotations));
    357         if (hasDefaultValue(annotations)) {
    358             result.append("[optional");
    359             if (hasExplicitDefaultValue(annotations)) {
    360                 result.append(", default " + getDefaultValue(parameterType, annotations));
    361             }
    362             result.append("]");
    363         }
    364 
    365         String description = getDescription(annotations);
    366         if (description.length() > 0) {
    367             result.append(": ");
    368             result.append(description);
    369         }
    370 
    371         return result.toString();
    372     }
    373 
    374     /**
    375      * Appends the name of the given type to the {@link StringBuilder}.
    376      *
    377      * @param builder string builder to append to
    378      * @param type    type whose name to append
    379      */
    380     private static void appendTypeName(final StringBuilder builder, final Type type) {
    381         if (type instanceof Class<?>) {
    382             builder.append(((Class<?>) type).getSimpleName());
    383         } else {
    384             ParameterizedType parametrizedType = (ParameterizedType) type;
    385             builder.append(((Class<?>) parametrizedType.getRawType()).getSimpleName());
    386             builder.append("<");
    387 
    388             Type[] arguments = parametrizedType.getActualTypeArguments();
    389             for (int i = 0; i < arguments.length; i++) {
    390                 if (i > 0) {
    391                     builder.append(", ");
    392                 }
    393                 appendTypeName(builder, arguments[i]);
    394             }
    395             builder.append(">");
    396         }
    397     }
    398 
    399     /**
    400      * Returns parameter descriptors suitable for the RPC call text representation.
    401      * <p>
    402      * <p>
    403      * Uses parameter value, default value or name, whatever is available first.
    404      *
    405      * @return an array of parameter descriptors
    406      */
    407     public ParameterDescriptor[] getParameterValues(String[] values) {
    408         Type[] parameterTypes = mMethod.getGenericParameterTypes();
    409         Annotation[][] parametersAnnotations = mMethod.getParameterAnnotations();
    410         ParameterDescriptor[] parameters = new ParameterDescriptor[parametersAnnotations.length];
    411         for (int index = 0; index < parameters.length; index++) {
    412             String value;
    413             if (index < values.length) {
    414                 value = values[index];
    415             } else if (hasDefaultValue(parametersAnnotations[index])) {
    416                 Object defaultValue =
    417                         getDefaultValue(parameterTypes[index], parametersAnnotations[index]);
    418                 if (defaultValue == null) {
    419                     value = null;
    420                 } else {
    421                     value = String.valueOf(defaultValue);
    422                 }
    423             } else {
    424                 value = getName(parametersAnnotations[index]);
    425             }
    426             parameters[index] = new ParameterDescriptor(value, parameterTypes[index]);
    427         }
    428         return parameters;
    429     }
    430 
    431     /**
    432      * Returns parameter hints.
    433      *
    434      * @return an array of parameter hints
    435      */
    436     public String[] getParameterHints() {
    437         Annotation[][] parametersAnnotations = mMethod.getParameterAnnotations();
    438         String[] hints = new String[parametersAnnotations.length];
    439         for (int index = 0; index < hints.length; index++) {
    440             String name = getName(parametersAnnotations[index]);
    441             String description = getDescription(parametersAnnotations[index]);
    442             String hint = "No paramenter description.";
    443             if (!name.equals("") && !description.equals("")) {
    444                 hint = name + ": " + description;
    445             } else if (!name.equals("")) {
    446                 hint = name;
    447             } else if (!description.equals("")) {
    448                 hint = description;
    449             }
    450             hints[index] = hint;
    451         }
    452         return hints;
    453     }
    454 
    455     /**
    456      * Extracts the formal parameter name from an annotation.
    457      *
    458      * @param annotations the annotations of the parameter
    459      * @return the formal name of the parameter
    460      */
    461     private static String getName(Annotation[] annotations) {
    462         for (Annotation a : annotations) {
    463             if (a instanceof RpcParameter) {
    464                 return ((RpcParameter) a).name();
    465             }
    466         }
    467         throw new IllegalStateException("No parameter name");
    468     }
    469 
    470     /**
    471      * Extracts the parameter description from its annotations.
    472      *
    473      * @param annotations the annotations of the parameter
    474      * @return the description of the parameter
    475      */
    476     private static String getDescription(Annotation[] annotations) {
    477         for (Annotation a : annotations) {
    478             if (a instanceof RpcParameter) {
    479                 return ((RpcParameter) a).description();
    480             }
    481         }
    482         throw new IllegalStateException("No parameter description");
    483     }
    484 
    485     /**
    486      * Returns the default value for a specific parameter.
    487      *
    488      * @param parameterType parameterType
    489      * @param annotations   annotations of the parameter
    490      */
    491     public static Object getDefaultValue(Type parameterType, Annotation[] annotations) {
    492         for (Annotation a : annotations) {
    493             if (a instanceof RpcDefault) {
    494                 RpcDefault defaultAnnotation = (RpcDefault) a;
    495                 Converter<?> converter = converterFor(parameterType, defaultAnnotation.converter());
    496                 return converter.convert(defaultAnnotation.value());
    497             } else if (a instanceof RpcOptional) {
    498                 return null;
    499             }
    500         }
    501         throw new IllegalStateException("No default value for " + parameterType);
    502     }
    503 
    504     @SuppressWarnings("rawtypes")
    505     private static Converter<?> converterFor(Type parameterType,
    506                                              Class<? extends Converter> converterClass) {
    507         if (converterClass == Converter.class) {
    508             Converter<?> converter = sConverters.get(parameterType);
    509             if (converter == null) {
    510                 throw new IllegalArgumentException(
    511                         "No predefined converter found for " + parameterType);
    512             }
    513             return converter;
    514         }
    515         try {
    516             Constructor<?> constructor = converterClass.getConstructor(new Class<?>[0]);
    517             return (Converter<?>) constructor.newInstance(new Object[0]);
    518         } catch (Exception e) {
    519             throw new IllegalArgumentException("Cannot create converter from "
    520                     + converterClass.getCanonicalName());
    521         }
    522     }
    523 
    524     /**
    525      * Determines whether or not this parameter has default value.
    526      *
    527      * @param annotations annotations of the parameter
    528      */
    529     public static boolean hasDefaultValue(Annotation[] annotations) {
    530         for (Annotation a : annotations) {
    531             if (a instanceof RpcDefault || a instanceof RpcOptional) {
    532                 return true;
    533             }
    534         }
    535         return false;
    536     }
    537 
    538     /**
    539      * Returns whether the default value is specified for a specific parameter.
    540      *
    541      * @param annotations annotations of the parameter
    542      */
    543     @VisibleForTesting
    544     static boolean hasExplicitDefaultValue(Annotation[] annotations) {
    545         for (Annotation a : annotations) {
    546             if (a instanceof RpcDefault) {
    547                 return true;
    548             }
    549         }
    550         return false;
    551     }
    552 
    553     /** Returns the converters for {@code String}, {@code Integer} and {@code Boolean}. */
    554     private static Map<Class<?>, Converter<?>> populateConverters() {
    555         Map<Class<?>, Converter<?>> converters = new HashMap<Class<?>, Converter<?>>();
    556         converters.put(String.class, new Converter<String>() {
    557             @Override
    558             public String convert(String value) {
    559                 return value;
    560             }
    561         });
    562         converters.put(Integer.class, new Converter<Integer>() {
    563             @Override
    564             public Integer convert(String input) {
    565                 try {
    566                     return Integer.decode(input);
    567                 } catch (NumberFormatException e) {
    568                     throw new IllegalArgumentException("'" + input + "' is not an integer");
    569                 }
    570             }
    571         });
    572         converters.put(Boolean.class, new Converter<Boolean>() {
    573             @Override
    574             public Boolean convert(String input) {
    575                 if (input == null) {
    576                     return null;
    577                 }
    578                 input = input.toLowerCase();
    579                 if (input.equals("true")) {
    580                     return Boolean.TRUE;
    581                 }
    582                 if (input.equals("false")) {
    583                     return Boolean.FALSE;
    584                 }
    585                 throw new IllegalArgumentException("'" + input + "' is not a boolean");
    586             }
    587         });
    588         return converters;
    589     }
    590 }
    591