AsmGeneratorpublic class AsmGenerator extends Object Class that generates a new JAR from a list of classes, some of which are to be kept as-is
and some of which are to be stubbed partially or totally. |
Fields Summary |
---|
private final Log | mLogOutput logger. | private final String | mOsDestJarThe path of the destination JAR to create. | private final Class[] | mInjectClassesList of classes to inject in the final JAR from _this_ archive. | private final Set | mStubMethodsThe set of methods to stub out. | private Map | mKeepAll classes to output as-is, except if they have native methods. | private Map | mDepsAll dependencies that must be completely stubbed. | private Map | mCopyFilesAll files that are to be copied as-is. | private Set | mReplaceMethodCallsClassesAll classes where certain method calls need to be rewritten. | private int | mRenameCountCounter of number of classes renamed during transform. | private final HashMap | mRenameClassesFQCN Names of the classes to rename: map old-FQCN => new-FQCN | private HashSet | mClassesNotRenamedFQCN Names of "old" classes that were NOT renamed. This starts with the full list of
old-FQCN to rename and they get erased as they get renamed. At the end, classes still
left here are not in the code base anymore and thus were not renamed. | private HashMap | mDeleteReturnsA map { FQCN => set { list of return types to delete from the FQCN } }. | private final HashMap | mDelegateMethodsA map { FQCN => set { method names } } of methods to rewrite as delegates.
The special name {@link DelegateClassAdapter#ALL_NATIVES} can be used as in internal set. | private final HashMap | mRefactorClassesFQCN Names of classes to refactor. All reference to old-FQCN will be updated to new-FQCN.
map old-FQCN => new-FQCN |
Constructors Summary |
---|
public AsmGenerator(Log log, String osDestJar, ICreateInfo createInfo)Creates a new generator that can generate the output JAR with the stubbed classes.
mLog = log;
mOsDestJar = osDestJar;
mInjectClasses = createInfo.getInjectedClasses();
mStubMethods = new HashSet<String>(Arrays.asList(createInfo.getOverriddenMethods()));
// Create the map/set of methods to change to delegates
mDelegateMethods = new HashMap<String, Set<String>>();
for (String signature : createInfo.getDelegateMethods()) {
int pos = signature.indexOf('#");
if (pos <= 0 || pos >= signature.length() - 1) {
continue;
}
String className = binaryToInternalClassName(signature.substring(0, pos));
String methodName = signature.substring(pos + 1);
Set<String> methods = mDelegateMethods.get(className);
if (methods == null) {
methods = new HashSet<String>();
mDelegateMethods.put(className, methods);
}
methods.add(methodName);
}
for (String className : createInfo.getDelegateClassNatives()) {
className = binaryToInternalClassName(className);
Set<String> methods = mDelegateMethods.get(className);
if (methods == null) {
methods = new HashSet<String>();
mDelegateMethods.put(className, methods);
}
methods.add(DelegateClassAdapter.ALL_NATIVES);
}
// Create the map of classes to rename.
mRenameClasses = new HashMap<String, String>();
mClassesNotRenamed = new HashSet<String>();
String[] renameClasses = createInfo.getRenamedClasses();
int n = renameClasses.length;
for (int i = 0; i < n; i += 2) {
assert i + 1 < n;
// The ASM class names uses "/" separators, whereas regular FQCN use "."
String oldFqcn = binaryToInternalClassName(renameClasses[i]);
String newFqcn = binaryToInternalClassName(renameClasses[i + 1]);
mRenameClasses.put(oldFqcn, newFqcn);
mClassesNotRenamed.add(oldFqcn);
}
// Create a map of classes to be refactored.
mRefactorClasses = new HashMap<String, String>();
String[] refactorClasses = createInfo.getJavaPkgClasses();
n = refactorClasses.length;
for (int i = 0; i < n; i += 2) {
assert i + 1 < n;
String oldFqcn = binaryToInternalClassName(refactorClasses[i]);
String newFqcn = binaryToInternalClassName(refactorClasses[i + 1]);
mRefactorClasses.put(oldFqcn, newFqcn);
}
// create the map of renamed class -> return type of method to delete.
mDeleteReturns = new HashMap<String, Set<String>>();
String[] deleteReturns = createInfo.getDeleteReturns();
Set<String> returnTypes = null;
String renamedClass = null;
for (String className : deleteReturns) {
// if we reach the end of a section, add it to the main map
if (className == null) {
if (returnTypes != null) {
mDeleteReturns.put(renamedClass, returnTypes);
}
renamedClass = null;
continue;
}
// if the renamed class is null, this is the beginning of a section
if (renamedClass == null) {
renamedClass = binaryToInternalClassName(className);
continue;
}
// just a standard return type, we add it to the list.
if (returnTypes == null) {
returnTypes = new HashSet<String>();
}
returnTypes.add(binaryToInternalClassName(className));
}
|
Methods Summary |
---|
java.lang.String | binaryToInternalClassName(java.lang.String className)Utility that returns the internal ASM class name from a fully qualified binary class
name. E.g. it returns android/view/View from android.view.View.
if (className == null) {
return null;
} else {
return className.replace('.", '/");
}
| java.lang.String | classNameToEntryPath(java.lang.String className)Utility method that converts a fully qualified java name into a JAR entry path
e.g. for the input "android.view.View" it returns "android/view/View.class"
return className.replaceAll("\\.", "/").concat(".class");
| private java.lang.String | classToEntryPath(java.lang.Class clazz)Utility method to get the JAR entry path from a Class name.
e.g. it returns something like "com/foo/OuterClass$InnerClass1$InnerClass2.class"
String name = "";
Class<?> parent;
while ((parent = clazz.getEnclosingClass()) != null) {
name = "$" + clazz.getSimpleName() + name;
clazz = parent;
}
return classNameToEntryPath(clazz.getCanonicalName() + name);
| void | createJar(java.io.FileOutputStream outStream, java.util.Map all)Writes the JAR file.
JarOutputStream jar = new JarOutputStream(outStream);
for (Entry<String, byte[]> entry : all.entrySet()) {
String name = entry.getKey();
JarEntry jar_entry = new JarEntry(name);
jar.putNextEntry(jar_entry);
jar.write(entry.getValue());
jar.closeEntry();
}
jar.flush();
jar.close();
| public void | generate()Generates the final JAR
TreeMap<String, byte[]> all = new TreeMap<String, byte[]>();
for (Class<?> clazz : mInjectClasses) {
String name = classToEntryPath(clazz);
InputStream is = ClassLoader.getSystemResourceAsStream(name);
ClassReader cr = new ClassReader(is);
byte[] b = transform(cr, true);
name = classNameToEntryPath(transformName(cr.getClassName()));
all.put(name, b);
}
for (Entry<String, ClassReader> entry : mDeps.entrySet()) {
ClassReader cr = entry.getValue();
byte[] b = transform(cr, true);
String name = classNameToEntryPath(transformName(cr.getClassName()));
all.put(name, b);
}
for (Entry<String, ClassReader> entry : mKeep.entrySet()) {
ClassReader cr = entry.getValue();
byte[] b = transform(cr, true);
String name = classNameToEntryPath(transformName(cr.getClassName()));
all.put(name, b);
}
for (Entry<String, InputStream> entry : mCopyFiles.entrySet()) {
try {
byte[] b = inputStreamToByteArray(entry.getValue());
all.put(entry.getKey(), b);
} catch (IOException e) {
// Ignore.
}
}
mLog.info("# deps classes: %d", mDeps.size());
mLog.info("# keep classes: %d", mKeep.size());
mLog.info("# renamed : %d", mRenameCount);
createJar(new FileOutputStream(mOsDestJar), all);
mLog.info("Created JAR file %s", mOsDestJar);
| public java.util.Set | getClassesNotRenamed()Returns the list of classes that have not been renamed yet.
The names are "internal class names" rather than FQCN, i.e. they use "/" instead "."
as package separators.
return mClassesNotRenamed;
| boolean | hasNativeMethods(org.objectweb.asm.ClassReader cr)Returns true if a class has any native methods.
ClassHasNativeVisitor cv = new ClassHasNativeVisitor();
cr.accept(cv, 0);
return cv.hasNativeMethods();
| private byte[] | inputStreamToByteArray(java.io.InputStream is)
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] data = new byte[8192]; // 8KB
int n;
while ((n = is.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, n);
}
return buffer.toByteArray();
| public void | setCopyFiles(java.util.Map copyFiles)Sets the map of files to output as-is.
mCopyFiles = copyFiles;
| public void | setDeps(java.util.Map deps)Sets the map of dependencies that must be completely stubbed
mDeps = deps;
| public void | setKeep(java.util.Map keep)Sets the map of classes to output as-is, except if they have native methods
mKeep = keep;
| public void | setRewriteMethodCallClasses(java.util.Set rewriteMethodCallClasses)
mReplaceMethodCallsClasses = rewriteMethodCallClasses;
| byte[] | transform(org.objectweb.asm.ClassReader cr, boolean stubNativesOnly)Transforms a class.
There are 3 kind of transformations:
1- For "mock" dependencies classes, we want to remove all code from methods and replace
by a stub. Native methods must be implemented with this stub too. Abstract methods are
left intact. Modified classes must be overridable (non-private, non-final).
Native methods must be made non-final, non-private.
2- For "keep" classes, we want to rewrite all native methods as indicated above.
If a class has native methods, it must also be made non-private, non-final.
Note that unfortunately static methods cannot be changed to non-static (since static and
non-static are invoked differently.)
boolean hasNativeMethods = hasNativeMethods(cr);
// Get the class name, as an internal name (e.g. com/android/SomeClass$InnerClass)
String className = cr.getClassName();
String newName = transformName(className);
// transformName returns its input argument if there's no need to rename the class
if (!newName.equals(className)) {
mRenameCount++;
// This class is being renamed, so remove it from the list of classes not renamed.
mClassesNotRenamed.remove(className);
}
mLog.debug("Transform %s%s%s%s", className,
newName.equals(className) ? "" : " (renamed to " + newName + ")",
hasNativeMethods ? " -- has natives" : "",
stubNativesOnly ? " -- stub natives only" : "");
// Rewrite the new class from scratch, without reusing the constant pool from the
// original class reader.
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = cw;
if (mReplaceMethodCallsClasses.contains(className)) {
cv = new ReplaceMethodCallsAdapter(cv);
}
cv = new RefactorClassAdapter(cv, mRefactorClasses);
if (!newName.equals(className)) {
cv = new RenameClassAdapter(cv, className, newName);
}
cv = new TransformClassAdapter(mLog, mStubMethods, mDeleteReturns.get(className),
newName, cv, stubNativesOnly);
Set<String> delegateMethods = mDelegateMethods.get(className);
if (delegateMethods != null && !delegateMethods.isEmpty()) {
// If delegateMethods only contains one entry ALL_NATIVES and the class is
// known to have no native methods, just skip this step.
if (hasNativeMethods ||
!(delegateMethods.size() == 1 &&
delegateMethods.contains(DelegateClassAdapter.ALL_NATIVES))) {
cv = new DelegateClassAdapter(mLog, cv, className, delegateMethods);
}
}
cr.accept(cv, 0);
return cw.toByteArray();
| java.lang.String | transformName(java.lang.String className)Should this class be renamed, this returns the new name. Otherwise it returns the
original name.
String newName = mRenameClasses.get(className);
if (newName != null) {
return newName;
}
int pos = className.indexOf('$");
if (pos > 0) {
// Is this an inner class of a renamed class?
String base = className.substring(0, pos);
newName = mRenameClasses.get(base);
if (newName != null) {
return newName + className.substring(pos);
}
}
return className;
|
|