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