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