1/* 2 * Copyright (C) 2010 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 */ 16package com.android.monkeyrunner; 17 18import java.lang.reflect.AccessibleObject; 19import java.lang.reflect.Field; 20import java.lang.reflect.Method; 21import java.lang.reflect.Modifier; 22import java.text.BreakIterator; 23import java.util.Arrays; 24import java.util.Collection; 25import java.util.Collections; 26import java.util.HashSet; 27import java.util.List; 28import java.util.Map; 29import java.util.Map.Entry; 30import java.util.Set; 31import java.util.logging.Level; 32import java.util.logging.Logger; 33 34import org.python.core.ArgParser; 35import org.python.core.ClassDictInit; 36import org.python.core.Py; 37import org.python.core.PyBoolean; 38import org.python.core.PyDictionary; 39import org.python.core.PyFloat; 40import org.python.core.PyInteger; 41import org.python.core.PyList; 42import org.python.core.PyNone; 43import org.python.core.PyObject; 44import org.python.core.PyReflectedField; 45import org.python.core.PyReflectedFunction; 46import org.python.core.PyString; 47import org.python.core.PyStringMap; 48import org.python.core.PyTuple; 49 50import com.android.monkeyrunner.doc.MonkeyRunnerExported; 51import com.google.common.base.Preconditions; 52import com.google.common.base.Predicate; 53import com.google.common.base.Predicates; 54import com.google.common.collect.Collections2; 55import com.google.common.collect.ImmutableMap; 56import com.google.common.collect.Lists; 57import com.google.common.collect.Maps; 58import com.google.common.collect.Sets; 59import com.google.common.collect.ImmutableMap.Builder; 60 61/** 62 * Collection of useful utilities function for interacting with the Jython interpreter. 63 */ 64public final class JythonUtils { 65 private static final Logger LOG = Logger.getLogger(JythonUtils.class.getCanonicalName()); 66 private JythonUtils() { } 67 68 /** 69 * Mapping of PyObject classes to the java class we want to convert them to. 70 */ 71 private static final Map<Class<? extends PyObject>, Class<?>> PYOBJECT_TO_JAVA_OBJECT_MAP; 72 static { 73 Builder<Class<? extends PyObject>, Class<?>> builder = ImmutableMap.builder(); 74 75 builder.put(PyString.class, String.class); 76 // What python calls float, most people call double 77 builder.put(PyFloat.class, Double.class); 78 builder.put(PyInteger.class, Integer.class); 79 builder.put(PyBoolean.class, Boolean.class); 80 81 PYOBJECT_TO_JAVA_OBJECT_MAP = builder.build(); 82 } 83 84 /** 85 * Utility method to be called from Jython bindings to give proper handling of keyword and 86 * positional arguments. 87 * 88 * @param args the PyObject arguments from the binding 89 * @param kws the keyword arguments from the binding 90 * @return an ArgParser for this binding, or null on error 91 */ 92 public static ArgParser createArgParser(PyObject[] args, String[] kws) { 93 StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); 94 // Up 2 levels in the current stack to give us the calling function 95 StackTraceElement element = stackTrace[2]; 96 97 String methodName = element.getMethodName(); 98 String className = element.getClassName(); 99 100 Class<?> clz; 101 try { 102 clz = Class.forName(className); 103 } catch (ClassNotFoundException e) { 104 LOG.log(Level.SEVERE, "Got exception: ", e); 105 return null; 106 } 107 108 Method m; 109 110 try { 111 m = clz.getMethod(methodName, PyObject[].class, String[].class); 112 } catch (SecurityException e) { 113 LOG.log(Level.SEVERE, "Got exception: ", e); 114 return null; 115 } catch (NoSuchMethodException e) { 116 LOG.log(Level.SEVERE, "Got exception: ", e); 117 return null; 118 } 119 120 MonkeyRunnerExported annotation = m.getAnnotation(MonkeyRunnerExported.class); 121 return new ArgParser(methodName, args, kws, 122 annotation.args()); 123 } 124 125 /** 126 * Get a python floating point value from an ArgParser. 127 * 128 * @param ap the ArgParser to get the value from. 129 * @param position the position in the parser 130 * @return the double value 131 */ 132 public static double getFloat(ArgParser ap, int position) { 133 PyObject arg = ap.getPyObject(position); 134 135 if (Py.isInstance(arg, PyFloat.TYPE)) { 136 return ((PyFloat) arg).asDouble(); 137 } 138 if (Py.isInstance(arg, PyInteger.TYPE)) { 139 return ((PyInteger) arg).asDouble(); 140 } 141 throw Py.TypeError("Unable to parse argument: " + position); 142 } 143 144 /** 145 * Get a python floating point value from an ArgParser. 146 * 147 * @param ap the ArgParser to get the value from. 148 * @param position the position in the parser 149 * @param defaultValue the default value to return if the arg isn't specified. 150 * @return the double value 151 */ 152 public static double getFloat(ArgParser ap, int position, double defaultValue) { 153 PyObject arg = ap.getPyObject(position, new PyFloat(defaultValue)); 154 155 if (Py.isInstance(arg, PyFloat.TYPE)) { 156 return ((PyFloat) arg).asDouble(); 157 } 158 if (Py.isInstance(arg, PyInteger.TYPE)) { 159 return ((PyInteger) arg).asDouble(); 160 } 161 throw Py.TypeError("Unable to parse argument: " + position); 162 } 163 164 /** 165 * Get a list of arguments from an ArgParser. 166 * 167 * @param ap the ArgParser 168 * @param position the position in the parser to get the argument from 169 * @return a list of those items 170 */ 171 @SuppressWarnings("unchecked") 172 public static List<Object> getList(ArgParser ap, int position) { 173 PyObject arg = ap.getPyObject(position, Py.None); 174 if (Py.isInstance(arg, PyNone.TYPE)) { 175 return Collections.emptyList(); 176 } 177 178 List<Object> ret = Lists.newArrayList(); 179 PyList array = (PyList) arg; 180 for (int x = 0; x < array.__len__(); x++) { 181 PyObject item = array.__getitem__(x); 182 183 Class<?> javaClass = PYOBJECT_TO_JAVA_OBJECT_MAP.get(item.getClass()); 184 if (javaClass != null) { 185 ret.add(item.__tojava__(javaClass)); 186 } 187 } 188 return ret; 189 } 190 191 /** 192 * Get a dictionary from an ArgParser. For ease of use, key types are always coerced to 193 * strings. If key type cannot be coeraced to string, an exception is raised. 194 * 195 * @param ap the ArgParser to work with 196 * @param position the position in the parser to get. 197 * @return a Map mapping the String key to the value 198 */ 199 public static Map<String, Object> getMap(ArgParser ap, int position) { 200 PyObject arg = ap.getPyObject(position, Py.None); 201 if (Py.isInstance(arg, PyNone.TYPE)) { 202 return Collections.emptyMap(); 203 } 204 205 Map<String, Object> ret = Maps.newHashMap(); 206 // cast is safe as getPyObjectbyType ensures it 207 PyDictionary dict = (PyDictionary) arg; 208 PyList items = dict.items(); 209 for (int x = 0; x < items.__len__(); x++) { 210 // It's a list of tuples 211 PyTuple item = (PyTuple) items.__getitem__(x); 212 // We call str(key) on the key to get the string and then convert it to the java string. 213 String key = (String) item.__getitem__(0).__str__().__tojava__(String.class); 214 PyObject value = item.__getitem__(1); 215 216 // Look up the conversion type and convert the value 217 Class<?> javaClass = PYOBJECT_TO_JAVA_OBJECT_MAP.get(value.getClass()); 218 if (javaClass != null) { 219 ret.put(key, value.__tojava__(javaClass)); 220 } 221 } 222 return ret; 223 } 224 225 private static PyObject convertObject(Object o) { 226 if (o instanceof String) { 227 return new PyString((String) o); 228 } else if (o instanceof Double) { 229 return new PyFloat((Double) o); 230 } else if (o instanceof Integer) { 231 return new PyInteger((Integer) o); 232 } else if (o instanceof Float) { 233 float f = (Float) o; 234 return new PyFloat(f); 235 } else if (o instanceof Boolean) { 236 return new PyBoolean((Boolean) o); 237 } 238 return Py.None; 239 } 240 241 /** 242 * Convert the given Java Map into a PyDictionary. 243 * 244 * @param map the map to convert 245 * @return the python dictionary 246 */ 247 public static PyDictionary convertMapToDict(Map<String, Object> map) { 248 Map<PyObject, PyObject> resultMap = Maps.newHashMap(); 249 250 for (Entry<String, Object> entry : map.entrySet()) { 251 resultMap.put(new PyString(entry.getKey()), 252 convertObject(entry.getValue())); 253 } 254 return new PyDictionary(resultMap); 255 } 256 257 /** 258 * This function should be called from classDictInit for any classes that are being exported 259 * to jython. This jython converts all the MonkeyRunnerExported annotations for the given class 260 * into the proper python form. It also removes any functions listed in the dictionary that 261 * aren't specifically annotated in the java class. 262 * 263 * NOTE: Make sure the calling class implements {@link ClassDictInit} to ensure that 264 * classDictInit gets called. 265 * 266 * @param clz the class to examine. 267 * @param dict the dictionary to update. 268 */ 269 public static void convertDocAnnotationsForClass(Class<?> clz, PyObject dict) { 270 Preconditions.checkNotNull(dict); 271 Preconditions.checkArgument(dict instanceof PyStringMap); 272 273 // See if the class has the annotation 274 if (clz.isAnnotationPresent(MonkeyRunnerExported.class)) { 275 MonkeyRunnerExported doc = clz.getAnnotation(MonkeyRunnerExported.class); 276 String fullDoc = buildClassDoc(doc, clz); 277 dict.__setitem__("__doc__", new PyString(fullDoc)); 278 } 279 280 // Get all the keys from the dict and put them into a set. As we visit the annotated methods, 281 // we will remove them from this set. At the end, these are the "hidden" methods that 282 // should be removed from the dict 283 Collection<String> functions = Sets.newHashSet(); 284 for (PyObject item : dict.asIterable()) { 285 functions.add(item.toString()); 286 } 287 288 // And remove anything that starts with __, as those are pretty important to retain 289 functions = Collections2.filter(functions, new Predicate<String>() { 290 @Override 291 public boolean apply(String value) { 292 return !value.startsWith("__"); 293 } 294 }); 295 296 // Look at all the methods in the class and find the one's that have the 297 // @MonkeyRunnerExported annotation. 298 for (Method m : clz.getMethods()) { 299 if (m.isAnnotationPresent(MonkeyRunnerExported.class)) { 300 String methodName = m.getName(); 301 PyObject pyFunc = dict.__finditem__(methodName); 302 if (pyFunc != null && pyFunc instanceof PyReflectedFunction) { 303 PyReflectedFunction realPyFunc = (PyReflectedFunction) pyFunc; 304 MonkeyRunnerExported doc = m.getAnnotation(MonkeyRunnerExported.class); 305 306 realPyFunc.__doc__ = new PyString(buildDoc(doc)); 307 functions.remove(methodName); 308 } 309 } 310 } 311 312 // Also look at all the fields (both static and instance). 313 for (Field f : clz.getFields()) { 314 if (f.isAnnotationPresent(MonkeyRunnerExported.class)) { 315 String fieldName = f.getName(); 316 PyObject pyField = dict.__finditem__(fieldName); 317 if (pyField != null && pyField instanceof PyReflectedField) { 318 PyReflectedField realPyfield = (PyReflectedField) pyField; 319 MonkeyRunnerExported doc = f.getAnnotation(MonkeyRunnerExported.class); 320 321 // TODO: figure out how to set field documentation. __doc__ is Read Only 322 // in this context. 323 // realPyfield.__setattr__("__doc__", new PyString(buildDoc(doc))); 324 functions.remove(fieldName); 325 } 326 } 327 } 328 329 // Now remove any elements left from the functions collection 330 for (String name : functions) { 331 dict.__delitem__(name); 332 } 333 } 334 335 private static final Predicate<AccessibleObject> SHOULD_BE_DOCUMENTED = new Predicate<AccessibleObject>() { 336 @Override 337 public boolean apply(AccessibleObject ao) { 338 return ao.isAnnotationPresent(MonkeyRunnerExported.class); 339 } 340 }; 341 private static final Predicate<Field> IS_FIELD_STATIC = new Predicate<Field>() { 342 @Override 343 public boolean apply(Field f) { 344 return (f.getModifiers() & Modifier.STATIC) != 0; 345 } 346 }; 347 348 /** 349 * build a jython doc-string for a class from the annotation and the fields 350 * contained within the class 351 * 352 * @param doc the annotation 353 * @param clz the class to be documented 354 * @return the doc-string 355 */ 356 private static String buildClassDoc(MonkeyRunnerExported doc, Class<?> clz) { 357 // Below the class doc, we need to document all the documented field this class contains 358 Collection<Field> annotatedFields = Collections2.filter(Arrays.asList(clz.getFields()), SHOULD_BE_DOCUMENTED); 359 Collection<Field> staticFields = Collections2.filter(annotatedFields, IS_FIELD_STATIC); 360 Collection<Field> nonStaticFields = Collections2.filter(annotatedFields, Predicates.not(IS_FIELD_STATIC)); 361 362 StringBuilder sb = new StringBuilder(); 363 for (String line : splitString(doc.doc(), 80)) { 364 sb.append(line).append("\n"); 365 } 366 367 if (staticFields.size() > 0) { 368 sb.append("\nClass Fields: \n"); 369 for (Field f : staticFields) { 370 sb.append(buildFieldDoc(f)); 371 } 372 } 373 374 if (nonStaticFields.size() > 0) { 375 sb.append("\n\nFields: \n"); 376 for (Field f : nonStaticFields) { 377 sb.append(buildFieldDoc(f)); 378 } 379 } 380 381 return sb.toString(); 382 } 383 384 /** 385 * Build a doc-string for the annotated field. 386 * 387 * @param f the field. 388 * @return the doc-string. 389 */ 390 private static String buildFieldDoc(Field f) { 391 MonkeyRunnerExported annotation = f.getAnnotation(MonkeyRunnerExported.class); 392 StringBuilder sb = new StringBuilder(); 393 int indentOffset = 2 + 3 + f.getName().length(); 394 String indent = makeIndent(indentOffset); 395 396 sb.append(" ").append(f.getName()).append(" - "); 397 398 boolean first = true; 399 for (String line : splitString(annotation.doc(), 80 - indentOffset)) { 400 if (first) { 401 first = false; 402 sb.append(line).append("\n"); 403 } else { 404 sb.append(indent).append(line).append("\n"); 405 } 406 } 407 408 409 return sb.toString(); 410 } 411 412 /** 413 * Build a jython doc-string from the MonkeyRunnerExported annotation. 414 * 415 * @param doc the annotation to build from 416 * @return a jython doc-string 417 */ 418 private static String buildDoc(MonkeyRunnerExported doc) { 419 Collection<String> docs = splitString(doc.doc(), 80); 420 StringBuilder sb = new StringBuilder(); 421 for (String d : docs) { 422 sb.append(d).append("\n"); 423 } 424 425 if (doc.args() != null && doc.args().length > 0) { 426 String[] args = doc.args(); 427 String[] argDocs = doc.argDocs(); 428 429 sb.append("\n Args:\n"); 430 for (int x = 0; x < doc.args().length; x++) { 431 sb.append(" ").append(args[x]); 432 if (argDocs != null && argDocs.length > x) { 433 sb.append(" - "); 434 int indentOffset = args[x].length() + 3 + 4; 435 Collection<String> lines = splitString(argDocs[x], 80 - indentOffset); 436 boolean first = true; 437 String indent = makeIndent(indentOffset); 438 for (String line : lines) { 439 if (first) { 440 first = false; 441 sb.append(line).append("\n"); 442 } else { 443 sb.append(indent).append(line).append("\n"); 444 } 445 } 446 } 447 } 448 } 449 450 return sb.toString(); 451 } 452 453 private static String makeIndent(int indentOffset) { 454 if (indentOffset == 0) { 455 return ""; 456 } 457 StringBuffer sb = new StringBuffer(); 458 while (indentOffset > 0) { 459 sb.append(' '); 460 indentOffset--; 461 } 462 return sb.toString(); 463 } 464 465 private static Collection<String> splitString(String source, int offset) { 466 BreakIterator boundary = BreakIterator.getLineInstance(); 467 boundary.setText(source); 468 469 List<String> lines = Lists.newArrayList(); 470 StringBuilder currentLine = new StringBuilder(); 471 int start = boundary.first(); 472 473 for (int end = boundary.next(); 474 end != BreakIterator.DONE; 475 start = end, end = boundary.next()) { 476 String b = source.substring(start, end); 477 if (currentLine.length() + b.length() < offset) { 478 currentLine.append(b); 479 } else { 480 // emit the old line 481 lines.add(currentLine.toString()); 482 currentLine = new StringBuilder(b); 483 } 484 } 485 lines.add(currentLine.toString()); 486 return lines; 487 } 488 489 /** 490 * Obtain the set of method names available from Python. 491 * 492 * @param clazz Class to inspect. 493 * @return set of method names annotated with {@code MonkeyRunnerExported}. 494 */ 495 public static Set<String> getMethodNames(Class<?> clazz) { 496 HashSet<String> methodNames = new HashSet<String>(); 497 for (Method m: clazz.getMethods()) { 498 if (m.isAnnotationPresent(MonkeyRunnerExported.class)) { 499 methodNames.add(m.getName()); 500 } 501 } 502 return methodNames; 503 } 504} 505