• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  */
16 package com.android.monkeyrunner;
17 
18 import java.lang.reflect.AccessibleObject;
19 import java.lang.reflect.Field;
20 import java.lang.reflect.Method;
21 import java.lang.reflect.Modifier;
22 import java.text.BreakIterator;
23 import java.util.Arrays;
24 import java.util.Collection;
25 import java.util.Collections;
26 import java.util.HashSet;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Map.Entry;
30 import java.util.Set;
31 import java.util.logging.Level;
32 import java.util.logging.Logger;
33 
34 import org.python.core.ArgParser;
35 import org.python.core.ClassDictInit;
36 import org.python.core.Py;
37 import org.python.core.PyBoolean;
38 import org.python.core.PyDictionary;
39 import org.python.core.PyFloat;
40 import org.python.core.PyInteger;
41 import org.python.core.PyList;
42 import org.python.core.PyNone;
43 import org.python.core.PyObject;
44 import org.python.core.PyReflectedField;
45 import org.python.core.PyReflectedFunction;
46 import org.python.core.PyString;
47 import org.python.core.PyStringMap;
48 import org.python.core.PyTuple;
49 
50 import com.android.monkeyrunner.doc.MonkeyRunnerExported;
51 import com.google.common.base.Preconditions;
52 import com.google.common.base.Predicate;
53 import com.google.common.base.Predicates;
54 import com.google.common.collect.Collections2;
55 import com.google.common.collect.ImmutableMap;
56 import com.google.common.collect.Lists;
57 import com.google.common.collect.Maps;
58 import com.google.common.collect.Sets;
59 import com.google.common.collect.ImmutableMap.Builder;
60 
61 /**
62  * Collection of useful utilities function for interacting with the Jython interpreter.
63  */
64 public final class JythonUtils {
65     private static final Logger LOG = Logger.getLogger(JythonUtils.class.getCanonicalName());
JythonUtils()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 
builder.put(PyString.class, String.class)75         builder.put(PyString.class, String.class);
76         // What python calls float, most people call double
builder.put(PyFloat.class, Double.class)77         builder.put(PyFloat.class, Double.class);
builder.put(PyInteger.class, Integer.class)78         builder.put(PyInteger.class, Integer.class);
builder.put(PyBoolean.class, Boolean.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      */
createArgParser(PyObject[] args, String[] kws)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      */
getFloat(ArgParser ap, int position)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      */
getFloat(ArgParser ap, int position, double defaultValue)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")
getList(ArgParser ap, int position)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      */
getMap(ArgParser ap, int position)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 
convertObject(Object o)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      */
convertMapToDict(Map<String, Object> map)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      */
convertDocAnnotationsForClass(Class<?> clz, PyObject dict)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      */
buildClassDoc(MonkeyRunnerExported doc, Class<?> clz)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      */
buildFieldDoc(Field f)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      */
buildDoc(MonkeyRunnerExported doc)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 
makeIndent(int indentOffset)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 
splitString(String source, int offset)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      */
getMethodNames(Class<?> clazz)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