content: /*
   * 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 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;
      /** 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 list of classes to be promoted to public visibility */
      private final Set<String> mPromotedClasses;
  
      /**
       * 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);
  
          mInjectedMethodsMap = createInfo.getInjectedMethodsMap();
  
          mPromotedClasses =
                  Arrays.stream(createInfo.getPromotedClasses()).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, 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_MAXS);
  
          ClassVisitor cv = cw;
  
          if (mReplaceMethodCallsClasses.contains(className)) {
              cv = new ReplaceMethodCallsAdapter(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));
          }
          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);
          }
          if (!mPromotedClasses.isEmpty()) {
              cv = new PromoteClassClassAdapter(cv, mPromotedClasses);
          }
  
          // Make sure no class file has a version above 52 (corresponding to Java 8),
          // so that layoutlib can be run with JDK 8.
          cv = new ChangeFileVersionAdapter(mLog, 52, 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();
      }
  }
  