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 */ 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 54 public MethodDescriptor(Class<? extends RpcReceiver> clazz, Method method) { 55 mClass = clazz; 56 mMethod = method; 57 } 58 59 @Override 60 public String toString() { 61 return mMethod.getDeclaringClass().getCanonicalName() + "." + mMethod.getName(); 62 } 63 64 /** Collects all methods with {@code RPC} annotation from given class. */ 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 */ 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 */ 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 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 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 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 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 276 public Method getMethod() { 277 return mMethod; 278 } 279 280 public Class<? extends RpcReceiver> getDeclaringClass() { 281 return mClass; 282 } 283 284 public String getName() { 285 if (mMethod.isAnnotationPresent(RpcName.class)) { 286 return mMethod.getAnnotation(RpcName.class).name(); 287 } 288 return mMethod.getName(); 289 } 290 291 public Type[] getGenericParameterTypes() { 292 return mMethod.getGenericParameterTypes(); 293 } 294 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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") 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 */ 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 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}. */ 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