blob: 98055e3a90e67a7228dd30ae866fcafee1f2b7dd [file] [log] [blame]
/*
* Copyright (C) 2008 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.tools.layoutlib.create;
import com.android.tools.layoutlib.annotations.NotNull;
import com.android.tools.layoutlib.create.AsmAnalyzer.Result;
import com.android.tools.layoutlib.create.ICreateInfo.MethodReplacer;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 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.
*/
public class AsmGenerator {
/** Output logger. */
private final Log mLog;
/** List of classes to inject in the final JAR from _this_ archive. */
private final Class<?>[] mInjectClasses;
/** All classes to output as-is, except if they have native methods. */
private Map<String, ClassReader> mKeep;
/** All dependencies that must be completely stubbed. */
private Map<String, ClassReader> mDeps;
private Map<String, ClassWriter> mDelegates = new HashMap<>();
/** All files that are to be copied as-is. */
private Map<String, InputStream> mCopyFiles;
/** All classes where certain method calls need to be rewritten. */
private Set<String> mReplaceMethodCallsClasses;
/** Counter of number of classes renamed during transform. */
private int mRenameCount;
/** FQCN Names of the classes to rename: map old-FQCN => new-FQCN */
private final HashMap<String, String> mRenameClasses;
/** FQCN 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 HashSet<String> mClassesNotRenamed;
/** A map { FQCN => set { list of return types to delete from the FQCN } }. */
private HashMap<String, Set<String>> mDeleteReturns;
/** A 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<String, Set<String>> mDelegateMethods;
/** FQCN Names of classes to refactor. All reference to old-FQCN will be updated to new-FQCN.
* map old-FQCN => new-FQCN */
private final HashMap<String, String> mRefactorClasses;
/** Methods to inject. FQCN of class in which method should be injected => runnable that does
* the injection. */
private final Map<String, ICreateInfo.InjectMethodRunnable> mInjectedMethodsMap;
/** A map { FQCN => set { field names } } which should be promoted to public visibility */
private final Map<String, Set<String>> mPromotedFields;
/** A map { FQCN => set { method names } } which should be promoted to public visibility */
private final Map<String, Set<String>> mPromotedMethods;
/** A list of classes to be promoted to public visibility */
private final Set<String> mPromotedClasses;
/** A set of classes for which NOT to delegate any native method */
private final Set<String> mKeepNativeClasses;
private final Set<String> mDelegateAllNative;
/** A set of classes for which to rename static initializers */
private Set<String> mRenameStaticInitializerClasses;
/** A Set of methods that should be intercepted and replaced **/
private final Set<MethodReplacer> mMethodReplacers;
private boolean mKeepAllNativeClasses;
/**
* Creates a new generator that can generate the output JAR with the stubbed classes.
*
* @param log Output logger.
* @param createInfo Creation parameters. Must not be null.
*/
public AsmGenerator(Log log, ICreateInfo createInfo) {
mLog = log;
ArrayList<Class<?>> injectedClasses =
new ArrayList<>(Arrays.asList(createInfo.getInjectedClasses()));
// Search for and add anonymous inner classes also.
ListIterator<Class<?>> iter = injectedClasses.listIterator();
while (iter.hasNext()) {
Class<?> clazz = iter.next();
try {
int i = 1;
while(i < 100) {
iter.add(Class.forName(clazz.getName() + "$" + i));
i++;
}
} catch (ClassNotFoundException ignored) {
// Expected.
}
}
mInjectClasses = injectedClasses.toArray(new Class<?>[0]);
// Create the map/set of methods to change to delegates
mDelegateMethods = new HashMap<>();
addToMap(createInfo.getDelegateMethods(), mDelegateMethods);
for (String className : createInfo.getDelegateClassNatives()) {
className = binaryToInternalClassName(className);
Set<String> methods = mDelegateMethods.get(className);
if (methods == null) {
methods = new HashSet<>();
mDelegateMethods.put(className, methods);
}
methods.add(DelegateClassAdapter.ALL_NATIVES);
}
// Create the map of classes to rename.
mRenameClasses = new HashMap<>();
mClassesNotRenamed = new HashSet<>();
String[] renameClasses = Stream.concat(
Arrays.stream(createInfo.getRenamedClasses()),
Arrays.stream(createInfo.getRefactoredClasses()))
.toArray(String[]::new);
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[] refactorClasses = Stream.concat(
Arrays.stream(createInfo.getJavaPkgClasses()),
Arrays.stream(createInfo.getRefactoredClasses()))
.toArray(String[]::new);
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[] 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<>();
}
returnTypes.add(binaryToInternalClassName(className));
}
mPromotedFields = new HashMap<>();
addToMap(createInfo.getPromotedFields(), mPromotedFields);
mPromotedMethods = new HashMap<>();
addToMap(createInfo.getPromotedMethods(), mPromotedMethods);
mInjectedMethodsMap = createInfo.getInjectedMethodsMap();
mPromotedClasses =
Arrays.stream(createInfo.getPromotedClasses()).collect(Collectors.toSet());
mKeepAllNativeClasses = createInfo.shouldKeepAllNativeClasses();
mKeepNativeClasses =
Arrays.stream(createInfo.getKeepClassNatives()).collect(Collectors.toSet());
mDelegateAllNative =
Arrays.stream(createInfo.getDelegateClassNativesToNatives()).collect(Collectors.toSet());
mMethodReplacers = Arrays.stream(createInfo.getMethodReplacers()).collect(Collectors.toSet());
mRenameStaticInitializerClasses =
Arrays.stream(createInfo.getDeferredStaticInitializerClasses()).collect(Collectors.toSet());
}
/**
* For each value in the array, split the value on '#' and add the parts to the map as key
* and value.
*/
private void addToMap(String[] entries, Map<String, Set<String>> map) {
for (String entry : entries) {
int pos = entry.indexOf('#');
if (pos <= 0 || pos >= entry.length() - 1) {
return;
}
String className = binaryToInternalClassName(entry.substring(0, pos));
String methodOrFieldName = entry.substring(pos + 1);
Set<String> set = map.get(className);
if (set == null) {
set = new HashSet<>();
map.put(className, set);
}
set.add(methodOrFieldName);
}
}
/**
* Returns the list of classes that have not been renamed yet.
* <p/>
* The names are "internal class names" rather than FQCN, i.e. they use "/" instead "."
* as package separators.
*/
public Set<String> getClassesNotRenamed() {
return mClassesNotRenamed;
}
/**
* 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.
*/
private String binaryToInternalClassName(String className) {
if (className == null) {
return null;
} else {
return className.replace('.', '/');
}
}
/** Generates the final JAR */
public Map<String, byte[]> generate() throws IOException {
TreeMap<String, byte[]> all = new TreeMap<>();
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, ClassWriter> entry : mDelegates.entrySet()) {
ClassWriter value = entry.getValue();
value.visitEnd();
String name = classNameToEntryPath(entry.getKey());
all.put(name, value.toByteArray());
}
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);
return all;
}
/**
* 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"
*/
String classNameToEntryPath(String className) {
return className.replace('.', '/').concat(".class");
}
/**
* Utility method to get the JAR entry path from a Class name.
* e.g. it returns something like "com/foo/OuterClass$InnerClass1$InnerClass2.class"
*/
private String classToEntryPath(Class<?> clazz) {
return classNameToEntryPath(clazz.getName());
}
/**
* Transforms a class.
* <p/>
* 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.)
*/
byte[] transform(ClassReader cr, boolean stubNativesOnly) {
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_FRAMES);
ClassVisitor cv = cw;
if (mReplaceMethodCallsClasses.contains(className)) {
cv = new ReplaceMethodCallsAdapter(mMethodReplacers, cv, className);
}
cv = new RefactorClassAdapter(cv, mRefactorClasses);
if (!newName.equals(className)) {
cv = new RenameClassAdapter(cv, className, newName);
}
String binaryNewName = newName.replace('/', '.');
if (mInjectedMethodsMap.keySet().contains(binaryNewName)) {
cv = new InjectMethodsAdapter(cv, mInjectedMethodsMap.get(binaryNewName));
}
if (mDelegateAllNative.contains(binaryNewName)) {
Set<String> delegateMethods = mDelegateMethods.remove(className);
if (delegateMethods != null && !delegateMethods.isEmpty()) {
cv = new DelegateClassAdapter(mLog, cv, className, delegateMethods);
}
cv = new DelegateToNativeAdapter(mLog, cv, className, mDelegates, delegateMethods);
}
else if (!mKeepAllNativeClasses && !mKeepNativeClasses.contains(binaryNewName)) {
cv = StubClassAdapter.builder(mLog, cv)
.withDeleteReturns(mDeleteReturns.get(className))
.withNewClassName(newName)
.useOnlyStubNative(stubNativesOnly)
.build();
}
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);
}
}
Set<String> promoteFields = mPromotedFields.get(className);
if (promoteFields != null && !promoteFields.isEmpty()) {
cv = new PromoteFieldClassAdapter(cv, promoteFields);
}
Set<String> promoteMethods = mPromotedMethods.get(className);
if (promoteMethods != null && !promoteMethods.isEmpty()) {
cv = new PromoteMethodClassAdapter(cv, promoteMethods);
}
if (!mPromotedClasses.isEmpty()) {
cv = new PromoteClassClassAdapter(cv, mPromotedClasses);
}
if (mRenameStaticInitializerClasses.contains(binaryNewName)) {
cv = new DeferStaticInitializerClassAdapter(cv);
}
// Make sure no class file has a version above 55 (corresponding to Java 11),
// so that layoutlib can be run with JDK 11.
cv = new ChangeFileVersionAdapter(mLog, 55, cv);
cr.accept(cv, 0);
return cw.toByteArray();
}
/**
* Should this class be renamed, this returns the new name. Otherwise it returns the
* original name.
*
* @param className The internal ASM name of the class that may have to be renamed
* @return A new transformed name or the original input argument.
*/
String transformName(String className) {
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;
}
/**
* Returns true if a class has any native methods.
*/
boolean hasNativeMethods(ClassReader cr) {
ClassHasNativeVisitor cv = new ClassHasNativeVisitor();
cr.accept(cv, 0);
return cv.hasNativeMethods();
}
private byte[] inputStreamToByteArray(InputStream is) throws IOException {
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();
}
/**
* Sets the inputs for the generator phase.
*/
public void setAnalysisResult(@NotNull Result analysisResult) {
// Map of classes to output as-is, except if they have native methods
mKeep = analysisResult.getFound();
// Map of dependencies that must be completely stubbed
mDeps = analysisResult.getDeps();
// Map of files to output as-is.
mCopyFiles = analysisResult.getFilesFound();
mReplaceMethodCallsClasses = analysisResult.getReplaceMethodCallClasses();
}
}