| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.monkeyrunner; |
| |
| import java.lang.reflect.AccessibleObject; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Method; |
| import java.lang.reflect.Modifier; |
| import java.text.BreakIterator; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| |
| import org.python.core.ArgParser; |
| import org.python.core.ClassDictInit; |
| import org.python.core.Py; |
| import org.python.core.PyBoolean; |
| import org.python.core.PyDictionary; |
| import org.python.core.PyFloat; |
| import org.python.core.PyInteger; |
| import org.python.core.PyList; |
| import org.python.core.PyNone; |
| import org.python.core.PyObject; |
| import org.python.core.PyReflectedField; |
| import org.python.core.PyReflectedFunction; |
| import org.python.core.PyString; |
| import org.python.core.PyStringMap; |
| import org.python.core.PyTuple; |
| |
| import com.android.monkeyrunner.doc.MonkeyRunnerExported; |
| import com.google.common.base.Preconditions; |
| import com.google.common.base.Predicate; |
| import com.google.common.base.Predicates; |
| import com.google.common.collect.Collections2; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Sets; |
| import com.google.common.collect.ImmutableMap.Builder; |
| |
| /** |
| * Collection of useful utilities function for interacting with the Jython interpreter. |
| */ |
| public final class JythonUtils { |
| private static final Logger LOG = Logger.getLogger(JythonUtils.class.getCanonicalName()); |
| private JythonUtils() { } |
| |
| /** |
| * Mapping of PyObject classes to the java class we want to convert them to. |
| */ |
| private static final Map<Class<? extends PyObject>, Class<?>> PYOBJECT_TO_JAVA_OBJECT_MAP; |
| static { |
| Builder<Class<? extends PyObject>, Class<?>> builder = ImmutableMap.builder(); |
| |
| builder.put(PyString.class, String.class); |
| // What python calls float, most people call double |
| builder.put(PyFloat.class, Double.class); |
| builder.put(PyInteger.class, Integer.class); |
| builder.put(PyBoolean.class, Boolean.class); |
| |
| PYOBJECT_TO_JAVA_OBJECT_MAP = builder.build(); |
| } |
| |
| /** |
| * Utility method to be called from Jython bindings to give proper handling of keyword and |
| * positional arguments. |
| * |
| * @param args the PyObject arguments from the binding |
| * @param kws the keyword arguments from the binding |
| * @return an ArgParser for this binding, or null on error |
| */ |
| public static ArgParser createArgParser(PyObject[] args, String[] kws) { |
| StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); |
| // Up 2 levels in the current stack to give us the calling function |
| StackTraceElement element = stackTrace[2]; |
| |
| String methodName = element.getMethodName(); |
| String className = element.getClassName(); |
| |
| Class<?> clz; |
| try { |
| clz = Class.forName(className); |
| } catch (ClassNotFoundException e) { |
| LOG.log(Level.SEVERE, "Got exception: ", e); |
| return null; |
| } |
| |
| Method m; |
| |
| try { |
| m = clz.getMethod(methodName, PyObject[].class, String[].class); |
| } catch (SecurityException e) { |
| LOG.log(Level.SEVERE, "Got exception: ", e); |
| return null; |
| } catch (NoSuchMethodException e) { |
| LOG.log(Level.SEVERE, "Got exception: ", e); |
| return null; |
| } |
| |
| MonkeyRunnerExported annotation = m.getAnnotation(MonkeyRunnerExported.class); |
| return new ArgParser(methodName, args, kws, |
| annotation.args()); |
| } |
| |
| /** |
| * Get a python floating point value from an ArgParser. |
| * |
| * @param ap the ArgParser to get the value from. |
| * @param position the position in the parser |
| * @return the double value |
| */ |
| public static double getFloat(ArgParser ap, int position) { |
| PyObject arg = ap.getPyObject(position); |
| |
| if (Py.isInstance(arg, PyFloat.TYPE)) { |
| return ((PyFloat) arg).asDouble(); |
| } |
| if (Py.isInstance(arg, PyInteger.TYPE)) { |
| return ((PyInteger) arg).asDouble(); |
| } |
| throw Py.TypeError("Unable to parse argument: " + position); |
| } |
| |
| /** |
| * Get a python floating point value from an ArgParser. |
| * |
| * @param ap the ArgParser to get the value from. |
| * @param position the position in the parser |
| * @param defaultValue the default value to return if the arg isn't specified. |
| * @return the double value |
| */ |
| public static double getFloat(ArgParser ap, int position, double defaultValue) { |
| PyObject arg = ap.getPyObject(position, new PyFloat(defaultValue)); |
| |
| if (Py.isInstance(arg, PyFloat.TYPE)) { |
| return ((PyFloat) arg).asDouble(); |
| } |
| if (Py.isInstance(arg, PyInteger.TYPE)) { |
| return ((PyInteger) arg).asDouble(); |
| } |
| throw Py.TypeError("Unable to parse argument: " + position); |
| } |
| |
| /** |
| * Get a list of arguments from an ArgParser. |
| * |
| * @param ap the ArgParser |
| * @param position the position in the parser to get the argument from |
| * @return a list of those items |
| */ |
| @SuppressWarnings("unchecked") |
| public static List<Object> getList(ArgParser ap, int position) { |
| PyObject arg = ap.getPyObject(position, Py.None); |
| if (Py.isInstance(arg, PyNone.TYPE)) { |
| return Collections.emptyList(); |
| } |
| |
| List<Object> ret = Lists.newArrayList(); |
| PyList array = (PyList) arg; |
| for (int x = 0; x < array.__len__(); x++) { |
| PyObject item = array.__getitem__(x); |
| |
| Class<?> javaClass = PYOBJECT_TO_JAVA_OBJECT_MAP.get(item.getClass()); |
| if (javaClass != null) { |
| ret.add(item.__tojava__(javaClass)); |
| } |
| } |
| return ret; |
| } |
| |
| /** |
| * Get a dictionary from an ArgParser. For ease of use, key types are always coerced to |
| * strings. If key type cannot be coeraced to string, an exception is raised. |
| * |
| * @param ap the ArgParser to work with |
| * @param position the position in the parser to get. |
| * @return a Map mapping the String key to the value |
| */ |
| public static Map<String, Object> getMap(ArgParser ap, int position) { |
| PyObject arg = ap.getPyObject(position, Py.None); |
| if (Py.isInstance(arg, PyNone.TYPE)) { |
| return Collections.emptyMap(); |
| } |
| |
| Map<String, Object> ret = Maps.newHashMap(); |
| // cast is safe as getPyObjectbyType ensures it |
| PyDictionary dict = (PyDictionary) arg; |
| PyList items = dict.items(); |
| for (int x = 0; x < items.__len__(); x++) { |
| // It's a list of tuples |
| PyTuple item = (PyTuple) items.__getitem__(x); |
| // We call str(key) on the key to get the string and then convert it to the java string. |
| String key = (String) item.__getitem__(0).__str__().__tojava__(String.class); |
| PyObject value = item.__getitem__(1); |
| |
| // Look up the conversion type and convert the value |
| Class<?> javaClass = PYOBJECT_TO_JAVA_OBJECT_MAP.get(value.getClass()); |
| if (javaClass != null) { |
| ret.put(key, value.__tojava__(javaClass)); |
| } |
| } |
| return ret; |
| } |
| |
| private static PyObject convertObject(Object o) { |
| if (o instanceof String) { |
| return new PyString((String) o); |
| } else if (o instanceof Double) { |
| return new PyFloat((Double) o); |
| } else if (o instanceof Integer) { |
| return new PyInteger((Integer) o); |
| } else if (o instanceof Float) { |
| float f = (Float) o; |
| return new PyFloat(f); |
| } else if (o instanceof Boolean) { |
| return new PyBoolean((Boolean) o); |
| } |
| return Py.None; |
| } |
| |
| /** |
| * Convert the given Java Map into a PyDictionary. |
| * |
| * @param map the map to convert |
| * @return the python dictionary |
| */ |
| public static PyDictionary convertMapToDict(Map<String, Object> map) { |
| Map<PyObject, PyObject> resultMap = Maps.newHashMap(); |
| |
| for (Entry<String, Object> entry : map.entrySet()) { |
| resultMap.put(new PyString(entry.getKey()), |
| convertObject(entry.getValue())); |
| } |
| return new PyDictionary(resultMap); |
| } |
| |
| /** |
| * This function should be called from classDictInit for any classes that are being exported |
| * to jython. This jython converts all the MonkeyRunnerExported annotations for the given class |
| * into the proper python form. It also removes any functions listed in the dictionary that |
| * aren't specifically annotated in the java class. |
| * |
| * NOTE: Make sure the calling class implements {@link ClassDictInit} to ensure that |
| * classDictInit gets called. |
| * |
| * @param clz the class to examine. |
| * @param dict the dictionary to update. |
| */ |
| public static void convertDocAnnotationsForClass(Class<?> clz, PyObject dict) { |
| Preconditions.checkNotNull(dict); |
| Preconditions.checkArgument(dict instanceof PyStringMap); |
| |
| // See if the class has the annotation |
| if (clz.isAnnotationPresent(MonkeyRunnerExported.class)) { |
| MonkeyRunnerExported doc = clz.getAnnotation(MonkeyRunnerExported.class); |
| String fullDoc = buildClassDoc(doc, clz); |
| dict.__setitem__("__doc__", new PyString(fullDoc)); |
| } |
| |
| // Get all the keys from the dict and put them into a set. As we visit the annotated methods, |
| // we will remove them from this set. At the end, these are the "hidden" methods that |
| // should be removed from the dict |
| Collection<String> functions = Sets.newHashSet(); |
| for (PyObject item : dict.asIterable()) { |
| functions.add(item.toString()); |
| } |
| |
| // And remove anything that starts with __, as those are pretty important to retain |
| functions = Collections2.filter(functions, new Predicate<String>() { |
| @Override |
| public boolean apply(String value) { |
| return !value.startsWith("__"); |
| } |
| }); |
| |
| // Look at all the methods in the class and find the one's that have the |
| // @MonkeyRunnerExported annotation. |
| for (Method m : clz.getMethods()) { |
| if (m.isAnnotationPresent(MonkeyRunnerExported.class)) { |
| String methodName = m.getName(); |
| PyObject pyFunc = dict.__finditem__(methodName); |
| if (pyFunc != null && pyFunc instanceof PyReflectedFunction) { |
| PyReflectedFunction realPyFunc = (PyReflectedFunction) pyFunc; |
| MonkeyRunnerExported doc = m.getAnnotation(MonkeyRunnerExported.class); |
| |
| realPyFunc.__doc__ = new PyString(buildDoc(doc)); |
| functions.remove(methodName); |
| } |
| } |
| } |
| |
| // Also look at all the fields (both static and instance). |
| for (Field f : clz.getFields()) { |
| if (f.isAnnotationPresent(MonkeyRunnerExported.class)) { |
| String fieldName = f.getName(); |
| PyObject pyField = dict.__finditem__(fieldName); |
| if (pyField != null && pyField instanceof PyReflectedField) { |
| PyReflectedField realPyfield = (PyReflectedField) pyField; |
| MonkeyRunnerExported doc = f.getAnnotation(MonkeyRunnerExported.class); |
| |
| // TODO: figure out how to set field documentation. __doc__ is Read Only |
| // in this context. |
| // realPyfield.__setattr__("__doc__", new PyString(buildDoc(doc))); |
| functions.remove(fieldName); |
| } |
| } |
| } |
| |
| // Now remove any elements left from the functions collection |
| for (String name : functions) { |
| dict.__delitem__(name); |
| } |
| } |
| |
| private static final Predicate<AccessibleObject> SHOULD_BE_DOCUMENTED = new Predicate<AccessibleObject>() { |
| @Override |
| public boolean apply(AccessibleObject ao) { |
| return ao.isAnnotationPresent(MonkeyRunnerExported.class); |
| } |
| }; |
| private static final Predicate<Field> IS_FIELD_STATIC = new Predicate<Field>() { |
| @Override |
| public boolean apply(Field f) { |
| return (f.getModifiers() & Modifier.STATIC) != 0; |
| } |
| }; |
| |
| /** |
| * build a jython doc-string for a class from the annotation and the fields |
| * contained within the class |
| * |
| * @param doc the annotation |
| * @param clz the class to be documented |
| * @return the doc-string |
| */ |
| private static String buildClassDoc(MonkeyRunnerExported doc, Class<?> clz) { |
| // Below the class doc, we need to document all the documented field this class contains |
| Collection<Field> annotatedFields = Collections2.filter(Arrays.asList(clz.getFields()), SHOULD_BE_DOCUMENTED); |
| Collection<Field> staticFields = Collections2.filter(annotatedFields, IS_FIELD_STATIC); |
| Collection<Field> nonStaticFields = Collections2.filter(annotatedFields, Predicates.not(IS_FIELD_STATIC)); |
| |
| StringBuilder sb = new StringBuilder(); |
| for (String line : splitString(doc.doc(), 80)) { |
| sb.append(line).append("\n"); |
| } |
| |
| if (staticFields.size() > 0) { |
| sb.append("\nClass Fields: \n"); |
| for (Field f : staticFields) { |
| sb.append(buildFieldDoc(f)); |
| } |
| } |
| |
| if (nonStaticFields.size() > 0) { |
| sb.append("\n\nFields: \n"); |
| for (Field f : nonStaticFields) { |
| sb.append(buildFieldDoc(f)); |
| } |
| } |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Build a doc-string for the annotated field. |
| * |
| * @param f the field. |
| * @return the doc-string. |
| */ |
| private static String buildFieldDoc(Field f) { |
| MonkeyRunnerExported annotation = f.getAnnotation(MonkeyRunnerExported.class); |
| StringBuilder sb = new StringBuilder(); |
| int indentOffset = 2 + 3 + f.getName().length(); |
| String indent = makeIndent(indentOffset); |
| |
| sb.append(" ").append(f.getName()).append(" - "); |
| |
| boolean first = true; |
| for (String line : splitString(annotation.doc(), 80 - indentOffset)) { |
| if (first) { |
| first = false; |
| sb.append(line).append("\n"); |
| } else { |
| sb.append(indent).append(line).append("\n"); |
| } |
| } |
| |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Build a jython doc-string from the MonkeyRunnerExported annotation. |
| * |
| * @param doc the annotation to build from |
| * @return a jython doc-string |
| */ |
| private static String buildDoc(MonkeyRunnerExported doc) { |
| Collection<String> docs = splitString(doc.doc(), 80); |
| StringBuilder sb = new StringBuilder(); |
| for (String d : docs) { |
| sb.append(d).append("\n"); |
| } |
| |
| if (doc.args() != null && doc.args().length > 0) { |
| String[] args = doc.args(); |
| String[] argDocs = doc.argDocs(); |
| |
| sb.append("\n Args:\n"); |
| for (int x = 0; x < doc.args().length; x++) { |
| sb.append(" ").append(args[x]); |
| if (argDocs != null && argDocs.length > x) { |
| sb.append(" - "); |
| int indentOffset = args[x].length() + 3 + 4; |
| Collection<String> lines = splitString(argDocs[x], 80 - indentOffset); |
| boolean first = true; |
| String indent = makeIndent(indentOffset); |
| for (String line : lines) { |
| if (first) { |
| first = false; |
| sb.append(line).append("\n"); |
| } else { |
| sb.append(indent).append(line).append("\n"); |
| } |
| } |
| } |
| } |
| } |
| |
| return sb.toString(); |
| } |
| |
| private static String makeIndent(int indentOffset) { |
| if (indentOffset == 0) { |
| return ""; |
| } |
| StringBuffer sb = new StringBuffer(); |
| while (indentOffset > 0) { |
| sb.append(' '); |
| indentOffset--; |
| } |
| return sb.toString(); |
| } |
| |
| private static Collection<String> splitString(String source, int offset) { |
| BreakIterator boundary = BreakIterator.getLineInstance(); |
| boundary.setText(source); |
| |
| List<String> lines = Lists.newArrayList(); |
| StringBuilder currentLine = new StringBuilder(); |
| int start = boundary.first(); |
| |
| for (int end = boundary.next(); |
| end != BreakIterator.DONE; |
| start = end, end = boundary.next()) { |
| String b = source.substring(start, end); |
| if (currentLine.length() + b.length() < offset) { |
| currentLine.append(b); |
| } else { |
| // emit the old line |
| lines.add(currentLine.toString()); |
| currentLine = new StringBuilder(b); |
| } |
| } |
| lines.add(currentLine.toString()); |
| return lines; |
| } |
| |
| /** |
| * Obtain the set of method names available from Python. |
| * |
| * @param clazz Class to inspect. |
| * @return set of method names annotated with {@code MonkeyRunnerExported}. |
| */ |
| public static Set<String> getMethodNames(Class<?> clazz) { |
| HashSet<String> methodNames = new HashSet<String>(); |
| for (Method m: clazz.getMethods()) { |
| if (m.isAnnotationPresent(MonkeyRunnerExported.class)) { |
| methodNames.add(m.getName()); |
| } |
| } |
| return methodNames; |
| } |
| } |