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