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