1 /* 2 * Copyright (C) 2016 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.googlecode.android_scripting.rpc; 18 19 import android.content.Intent; 20 import android.net.Uri; 21 import android.os.Bundle; 22 import android.os.Parcelable; 23 24 import com.googlecode.android_scripting.facade.AndroidFacade; 25 import com.googlecode.android_scripting.jsonrpc.RpcReceiver; 26 import com.googlecode.android_scripting.jsonrpc.RpcReceiverManager; 27 import com.googlecode.android_scripting.util.VisibleForTesting; 28 29 import java.lang.annotation.Annotation; 30 import java.lang.reflect.Constructor; 31 import java.lang.reflect.Method; 32 import java.lang.reflect.ParameterizedType; 33 import java.lang.reflect.Type; 34 import java.util.ArrayList; 35 import java.util.Collection; 36 import java.util.HashMap; 37 import java.util.List; 38 import java.util.Map; 39 40 import org.json.JSONArray; 41 import org.json.JSONException; 42 import org.json.JSONObject; 43 44 /** 45 * An adapter that wraps {@code Method}. 46 * 47 * @author igor.v.karp (at) gmail.com (Igor Karp) 48 */ 49 public final class MethodDescriptor { 50 private static final Map<Class<?>, Converter<?>> sConverters = populateConverters(); 51 52 private final Method mMethod; 53 private final Class<? extends RpcReceiver> mClass; 54 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