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