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