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@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 
MethodDescriptor(Class<? extends RpcReceiver> clazz, Method method)55   public MethodDescriptor(Class<? extends RpcReceiver> clazz, Method method) {
56     mClass = clazz;
57     mMethod = method;
58   }
59 
60   @Override
toString()61   public String toString() {
62     return mMethod.getDeclaringClass().getCanonicalName() + "." + mMethod.getName();
63   }
64 
65   /** Collects all methods with {@code RPC} annotation from given class. */
collectFrom(Class<? extends RpcReceiver> clazz)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    */
invoke(RpcReceiverManager manager, final JSONArray parameters)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    */
invoke(RpcReceiverManager manager, final Bundle parameters)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 
invoke(RpcReceiverManager manager, Object[] args)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
convertParameter(final JSONArray parameters, int index, Type type)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 
convertParameter(Bundle bundle, Class<?> type, String name)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 
buildIntent(JSONObject jsonObject)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 
getMethod()277   public Method getMethod() {
278     return mMethod;
279   }
280 
getDeclaringClass()281   public Class<? extends RpcReceiver> getDeclaringClass() {
282     return mClass;
283   }
284 
getName()285   public String getName() {
286     if (mMethod.isAnnotationPresent(RpcName.class)) {
287       return mMethod.getAnnotation(RpcName.class).name();
288     }
289     return mMethod.getName();
290   }
291 
getGenericParameterTypes()292   public Type[] getGenericParameterTypes() {
293     return mMethod.getGenericParameterTypes();
294   }
295 
getParameterAnnotations()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    */
getHelp()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    */
getHelpForParameter(Type parameterType, Annotation[] annotations)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    */
appendTypeName(final StringBuilder builder, final Type type)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    */
getParameterValues(String[] values)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    */
getParameterHints()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    */
getName(Annotation[] annotations)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    */
getDescription(Annotation[] annotations)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    */
getDefaultValue(Type parameterType, Annotation[] annotations)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")
converterFor(Type parameterType, Class<? extends Converter> converterClass)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    */
hasDefaultValue(Annotation[] annotations)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
hasExplicitDefaultValue(Annotation[] annotations)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}. */
populateConverters()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