|
| 1 | +""" |
| 2 | +Introspection functions for reporting Java |
| 3 | +class methods, fields, and source code URL. |
| 4 | +""" |
| 5 | + |
| 6 | +from typing import Any, Dict, List |
| 7 | + |
| 8 | +from scyjava._jvm import jimport, jvm_version |
| 9 | +from scyjava._types import isjava, jinstance, jclass |
| 10 | + |
| 11 | + |
| 12 | +def jreflect(data, aspect: str = "all") -> List[Dict[str, Any]]: |
| 13 | + """ |
| 14 | + Use Java reflection to introspect the given Java object, |
| 15 | + returning a table of its available methods or fields. |
| 16 | +
|
| 17 | + :param data: The object or class or fully qualified class name to inspect. |
| 18 | + :param aspect: One of: "all", "constructors", "fields", or "methods". |
| 19 | + :return: List of dicts with keys: "name", "mods", "arguments", and "returns". |
| 20 | + """ |
| 21 | + |
| 22 | + aspects = ["all", "constructors", "fields", "methods"] |
| 23 | + if aspect not in aspects: |
| 24 | + raise ValueError("aspect must be one of {aspects}") |
| 25 | + |
| 26 | + if not isjava(data) and isinstance(data, str): |
| 27 | + try: |
| 28 | + data = jimport(data) |
| 29 | + except Exception as e: |
| 30 | + raise ValueError( |
| 31 | + f"Object of type '{type(data).__name__}' is not a Java object" |
| 32 | + ) from e |
| 33 | + |
| 34 | + jcls = data if jinstance(data, "java.lang.Class") else jclass(data) |
| 35 | + |
| 36 | + Modifier = jimport("java.lang.reflect.Modifier") |
| 37 | + modifiers = { |
| 38 | + attr[2:].lower(): getattr(Modifier, attr) |
| 39 | + for attr in dir(Modifier) |
| 40 | + if attr.startswith("is") |
| 41 | + } |
| 42 | + |
| 43 | + members = [] |
| 44 | + if aspect in ["all", "constructors"]: |
| 45 | + members.extend(jcls.getConstructors()) |
| 46 | + if aspect in ["all", "fields"]: |
| 47 | + members.extend(jcls.getFields()) |
| 48 | + if aspect in ["all", "methods"]: |
| 49 | + members.extend(jcls.getMethods()) |
| 50 | + |
| 51 | + table = [] |
| 52 | + |
| 53 | + for member in members: |
| 54 | + mtype = str(member.getClass().getName()).split(".")[-1].lower() |
| 55 | + name = member.getName() |
| 56 | + modflags = member.getModifiers() |
| 57 | + mods = [name for name, hasmod in modifiers.items() if hasmod(modflags)] |
| 58 | + args = ( |
| 59 | + [ptype.getName() for ptype in member.getParameterTypes()] |
| 60 | + if hasattr(member, "getParameterTypes") |
| 61 | + else None |
| 62 | + ) |
| 63 | + returns = ( |
| 64 | + member.getReturnType().getName() |
| 65 | + if hasattr(member, "getReturnType") |
| 66 | + else (member.getType().getName() if hasattr(member, "getType") else name) |
| 67 | + ) |
| 68 | + table.append( |
| 69 | + { |
| 70 | + "type": mtype, |
| 71 | + "name": name, |
| 72 | + "mods": mods, |
| 73 | + "arguments": args, |
| 74 | + "returns": returns, |
| 75 | + } |
| 76 | + ) |
| 77 | + |
| 78 | + return table |
| 79 | + |
| 80 | + |
| 81 | +def jsource(data) -> str: |
| 82 | + """ |
| 83 | + Try to find the source code URL for the given Java object, class, or class name. |
| 84 | + Requires org.scijava:scijava-search on the classpath. |
| 85 | + :param data: |
| 86 | + Object, class, or fully qualified class name for which to discern the source code location. |
| 87 | + :return: URL of the class's source code. |
| 88 | + """ |
| 89 | + |
| 90 | + if not isjava(data) and isinstance(data, str): |
| 91 | + try: |
| 92 | + data = jimport(data) # check if data can be imported |
| 93 | + except Exception as err: |
| 94 | + raise ValueError(f"Not a Java object {err}") |
| 95 | + jcls = data if jinstance(data, "java.lang.Class") else jclass(data) |
| 96 | + |
| 97 | + if jcls.getClassLoader() is None: |
| 98 | + # Class is from the Java standard library. |
| 99 | + cls_path = str(jcls.getName()).replace(".", "/") |
| 100 | + |
| 101 | + # Discern the Java version. |
| 102 | + java_version = jvm_version()[0] |
| 103 | + |
| 104 | + # Note: some classes (e.g. corba and jaxp) will not be located correctly before |
| 105 | + # Java 10, because they fall under a different subtree than `jdk`. But Java 11+ |
| 106 | + # dispenses with such subtrees in favor of using only the module designations. |
| 107 | + if java_version <= 7: |
| 108 | + return f"https://github.com/openjdk/jdk/blob/jdk7-b147/jdk/src/share/classes/{cls_path}.java" |
| 109 | + elif java_version == 8: |
| 110 | + return f"https://github.com/openjdk/jdk/blob/jdk8-b120/jdk/src/share/classes/{cls_path}.java" |
| 111 | + else: # java_version >= 9 |
| 112 | + module_name = jcls.getModule().getName() |
| 113 | + # if module_name is null, it's in the unnamed module |
| 114 | + if java_version == 9: |
| 115 | + suffix = "%2B181/jdk" |
| 116 | + elif java_version == 10: |
| 117 | + suffix = "%2B46" |
| 118 | + else: |
| 119 | + suffix = "-ga" |
| 120 | + return f"https://github.com/openjdk/jdk/blob/jdk-{java_version}{suffix}/src/{module_name}/share/classes/{cls_path}.java" |
| 121 | + |
| 122 | + # Ask scijava-search for the source location. |
| 123 | + SourceFinder = jimport("org.scijava.search.SourceFinder") |
| 124 | + url = SourceFinder.sourceLocation(jcls, None) |
| 125 | + urlstring = url.toString() |
| 126 | + return urlstring |
0 commit comments