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