diff --git a/.gitignore b/.gitignore index 427a564..c850307 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ -*.jar +jar2xml.jar obj +tmpout/*.xml +annotations/*.xml +scraper.exe +scraper.exe.mdb diff --git a/JavaArchive.java b/JavaArchive.java index b4a543f..01969d3 100644 --- a/JavaArchive.java +++ b/JavaArchive.java @@ -25,6 +25,7 @@ package jar2xml; import java.io.File; +import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader; import java.util.jar.JarFile; @@ -34,6 +35,8 @@ import java.util.Enumeration; import java.util.HashMap; import java.util.List; +import org.objectweb.asm.*; +import org.objectweb.asm.tree.*; public class JavaArchive { @@ -61,14 +64,21 @@ public List getPackages () if (name.endsWith (".class")) { name = name.substring (0, name.length () - 6); try { + InputStream stream = file.getInputStream (entry); + ClassReader reader = new ClassReader (stream); + ClassNode node = new ClassNode (); + reader.accept (node, 0); + Class c = loader.loadClass (name.replace ('/', '.')); - String pkgname = c.getPackage ().getName (); + //String pkgname = c.getPackage ().getName (); + String pkgname = name.substring (0, name.lastIndexOf ('/')).replace ('/', '.'); + JavaPackage pkg = packages.get (pkgname); if (pkg == null) { pkg = new JavaPackage (pkgname); packages.put (pkgname, pkg); } - pkg.addClass (new JavaClass (c)); + pkg.addClass (new JavaClass (c, node)); } catch (Throwable t) { System.err.println ("Couldn't load class " + name); } diff --git a/JavaClass.java b/JavaClass.java index 2845801..376ca3c 100644 --- a/JavaClass.java +++ b/JavaClass.java @@ -42,18 +42,26 @@ import java.util.regex.Matcher; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.objectweb.asm.*; +import org.objectweb.asm.tree.*; public class JavaClass implements Comparable { private Class jclass; + private ClassNode asm; + private Map asmFields; private List deprecatedFields; private List deprecatedMethods; - public JavaClass (Class jclass) + public JavaClass (Class jclass, ClassNode asm) { this.jclass = jclass; deprecatedFields = AndroidDocScraper.getDeprecatedFields (jclass); deprecatedMethods = AndroidDocScraper.getDeprecatedMethods (jclass); + asmFields = new HashMap (); + + for (FieldNode fn : (List) asm.fields) + asmFields.put (fn.name, fn); } public int compareTo (JavaClass jc) @@ -134,7 +142,7 @@ int getConstructorParameterOffset (Constructor ctor) return 0; } - void appendField (Field field, Document doc, Element parent) + void appendField (Field field, FieldNode asmField, Document doc, Element parent) { int mods = field.getModifiers (); if (!Modifier.isPublic (mods) && !Modifier.isProtected (mods)) @@ -153,24 +161,40 @@ void appendField (Field field, Document doc, Element parent) e.setAttribute ("visibility", Modifier.isPublic (mods) ? "public" : "protected"); e.setAttribute ("volatile", Modifier.isVolatile (mods) ? "true" : "false"); setDeprecatedAttr (e, field.getDeclaredAnnotations (), e.getAttribute ("name")); - if (Modifier.isStatic (mods) && Modifier.isFinal (mods) && Modifier.isPublic (mods)) { + + // *** constant value retrieval *** + // sadly, there is no perfect solution: + // - basically we want to use ASM, but sometimes ASM fails + // to create FieldNode instance. + // - on the other hand, reflection + // - does not allow access to protected fields. + // - sometimes returns "default" value for "undefined" + // values such as 0 for ints and false for boolean. + // + // basically we use ASM here. + + if (asmField == null) + // this happens to couple of fields on java.awt.font.TextAttribute, java.lang.Double/Float and so on. + System.err.println ("!!!!! WARNING!!! null ASM FieldNode for " + field); + else if (asmField.value != null) { String type = e.getAttribute ("type"); + boolean isPublic = Modifier.isPublic (mods); try { if (type == "int") - e.setAttribute ("value", String.format ("%d", field.getInt (null))); + e.setAttribute ("value", String.format ("%d", asmField.value)); else if (type == "byte") - e.setAttribute ("value", String.format ("%d", field.getByte (null))); + e.setAttribute ("value", String.format ("%d", asmField.value)); else if (type == "char") - e.setAttribute ("value", String.format ("%d", (int) field.getChar (null))); + e.setAttribute ("value", String.format ("%d", asmField.value)); else if (type == "short") - e.setAttribute ("value", String.format ("%d", field.getShort (null))); + e.setAttribute ("value", String.format ("%d", asmField.value)); else if (type == "long") - e.setAttribute ("value", String.format ("%dL", field.getLong (null))); + e.setAttribute ("value", String.format ("%dL", asmField.value)); else if (type == "float") - e.setAttribute ("value", String.format ("%f", field.getFloat (null))); + e.setAttribute ("value", String.format ("%f", isPublic ? field.getFloat (null) : asmField.value)); else if (type == "double") { // see java.lang.Double constants. - double dvalue = field.getDouble (null); + double dvalue = (Double) asmField.value; String svalue; if (dvalue == Double.MAX_VALUE) @@ -184,13 +208,16 @@ else if (dvalue == Double.POSITIVE_INFINITY) else if (dvalue == Double.NEGATIVE_INFINITY) svalue = "(-1.0 / 0.0)"; else - svalue = String.format ("%f", dvalue); + // FIXME: here we specify "limited" digits for formatting. + // This should fix most cases, but this could still result in not-precise value. + // Math.E and Math.PI works with this. + svalue = String.format ("%.15f", dvalue); e.setAttribute ("value", svalue); } else if (type == "boolean") - e.setAttribute ("value", field.getBoolean (null) ? "true" : "false"); + e.setAttribute ("value", 0 == (Integer) asmField.value ? "true" : "false"); else if (type == "java.lang.String") { - String value = (String) field.get (null); + String value = (String) asmField.value; if (value != null) e.setAttribute ("value", "\"" + value.replace ("\\", "\\\\") + "\""); } @@ -467,7 +494,7 @@ else if (hret != null && !mret.isAssignableFrom (hret)) { Field [] fields = jclass.getDeclaredFields (); sortFields (fields); for (Field field : fields) - appendField (field, doc, e); + appendField (field, asmFields.get (field.getName ()), doc, e); } parent.appendChild (e); } diff --git a/MANIFEST.MF b/MANIFEST.MF new file mode 100644 index 0000000..b65ee14 --- /dev/null +++ b/MANIFEST.MF @@ -0,0 +1,4 @@ +Manifest-Version: 1.0 +Main-Class: jar2xml.Start +Class-Path: asm-debug-all-3.3.1.jar + diff --git a/Makefile b/Makefile index d72fb75..ac52e43 100644 --- a/Makefile +++ b/Makefile @@ -17,11 +17,11 @@ sources = \ JavaPackage.java \ Start.java -$(TARGET): $(sources) +$(TARGET): $(sources) MANIFEST.MF -rm -rf obj mkdir -p obj - javac -g -d obj $(sources) - jar cfe "$@" jar2xml.Start -C obj/ . + javac -g -d obj $(sources) -cp asm-debug-all-4.0_RC1.jar + jar cfm "$@" MANIFEST.MF asm-debug-all-4.0_RC1.jar -C obj/ . scraper.exe : scraper.cs mcs -debug scraper.cs diff --git a/README b/README index 05ab27b..06bdf69 100644 --- a/README +++ b/README @@ -8,6 +8,7 @@ and associated documentation. The information is stored in XML format. - xmllint (for doc scraping) - xmlstarlet (ditto) + Also it uses ASM (included in the sources) * Tools @@ -77,21 +78,6 @@ and associated documentation. The information is stored in XML format. excluded, but this brings another check to *not* exclude methods when the corresponding base methods are excluded). -** Byte code engineering - -- Some fields seem to be incorrectly marked as constant. For example, - android.os.Build.TIME is not a constant, but since the field.getLong() - returns 0, it is set as 0 (and it does not return null for get()). - - java.io.File.pathSeparatorChar has the same problem, but it is worse; - it returns non-zero value so we cannot depend on the value. - - Solution ideas: scrape constant values, or use ASM. - -- Constant values for "protected" fields need to be retrieved, but Java reflection API throws IllegalAccessException for such attempt. - - This likely has to be resolved by bytecode engineering (such as ASM). - ** Method override resolution - Do not skip certain overriden methods. This in JavaClass.java gives @@ -233,3 +219,23 @@ Now we have version-aware scraper and resolved the issue described below. A possible approach is to checkout API docs for *every* version (from AOSP). + + +** Byte code engineering + +Now we use ASM to solve the issues below: + + +- Some fields were incorrectly marked as constant. For example, + android.os.Build.TIME is not a constant, but since the field.getLong() + returns 0, it is set as 0 (and it does not return null for get()). + + java.io.File.pathSeparatorChar has the same problem, but it is worse; + it returns non-zero value so we cannot depend on the value. + + Solution ideas: scrape constant values, or use ASM. + +- Constant values for "protected" fields need to be retrieved, but Java reflection API throws IllegalAccessException for such attempt. + + This likely has to be resolved by bytecode engineering (such as ASM). + diff --git a/asm-debug-all-4.0_RC1.jar b/asm-debug-all-4.0_RC1.jar new file mode 100644 index 0000000..d5aa15e Binary files /dev/null and b/asm-debug-all-4.0_RC1.jar differ