package net.bytebuddy.build.gradle; import net.bytebuddy.ByteBuddy; import net.bytebuddy.build.EntryPoint; import net.bytebuddy.build.Plugin; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.dynamic.ClassFileLocator; import net.bytebuddy.dynamic.DynamicType; import net.bytebuddy.dynamic.scaffold.inline.MethodNameTransformer; import net.bytebuddy.implementation.LoadedTypeInitializer; import net.bytebuddy.pool.TypePool; import org.gradle.api.Action; import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.tasks.compile.AbstractCompile; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * Applies a transformation to the classes that were generated by a compilation task. */ public class TransformationAction implements Action<Task> { /** * The class file extension for Java files. */ private static final String CLASS_FILE_EXTENSION = ".class"; /** * The current project. */ private final Project project; /** * The current project's Byte Buddy extension. */ private final ByteBuddyExtension byteBuddyExtension; /** * The task to which this transformation was appended. */ private final AbstractCompile task; /** * Creates a new transformation action. * * @param project The current project. * @param extension The current project's Byte Buddy extension. * @param task The task to which this transformation was appended. */ public TransformationAction(Project project, ByteBuddyExtension extension, AbstractCompile task) { this.project = project; this.byteBuddyExtension = extension; this.task = task; } @Override public void execute(Task task) { ByteBuddyLogHandler byteBuddyLogHandler = ByteBuddyLogHandler.initialize(project); try { processOutputDirectory(this.task.getDestinationDir(), this.task.getClasspath()); } catch (IOException exception) { throw new GradleException("Error accessing file system", exception); } finally { byteBuddyLogHandler.reset(); } } /** * Processes all class files within the given directory. * * @param root The root directory to process. * @param classPath A list of class path elements expected by the processed classes. * @throws IOException If an I/O exception occurs. */ private void processOutputDirectory(File root, Iterable<? extends File> classPath) throws IOException { if (!root.isDirectory()) { throw new GradleException("Target location does not exist or is no directory: " + root); } ClassLoaderResolver classLoaderResolver = new ClassLoaderResolver(); try { List<Plugin> plugins = new ArrayList<Plugin>(byteBuddyExtension.getTransformations().size()); for (Transformation transformation : byteBuddyExtension.getTransformations()) { String plugin = transformation.getPlugin(); try { plugins.add((Plugin) Class.forName(plugin, false, classLoaderResolver.resolve(transformation.getClassPath(root, classPath))) .getDeclaredConstructor() .newInstance()); project.getLogger().info("Created plugin: {}", plugin); } catch (Exception exception) { if (exception instanceof GradleException) { throw (GradleException) exception; } throw new GradleException("Cannot create plugin: " + transformation.getRawPlugin(), exception); } } EntryPoint entryPoint = byteBuddyExtension.getInitialization().getEntryPoint(classLoaderResolver, root, classPath); project.getLogger().info("Resolved entry point: {}", entryPoint); transform(root, classPath, entryPoint, plugins); } finally { classLoaderResolver.close(); } } /** * Applies all registered transformations. * * @param root The root directory to process. * @param entryPoint The transformation's entry point. * @param classPath A list of class path elements expected by the processed classes. * @param plugins The plugins to apply. * @throws IOException If an I/O exception occurs. */ private void transform(File root, Iterable<? extends File> classPath, EntryPoint entryPoint, List<Plugin> plugins) throws IOException { List<ClassFileLocator> classFileLocators = new ArrayList<ClassFileLocator>(); classFileLocators.add(new ClassFileLocator.ForFolder(root)); for (File artifact : classPath) { classFileLocators.add(artifact.isFile() ? ClassFileLocator.ForJarFile.of(artifact) : new ClassFileLocator.ForFolder(artifact)); } ClassFileLocator classFileLocator = new ClassFileLocator.Compound(classFileLocators); try { TypePool typePool = new TypePool.Default.WithLazyResolution(new TypePool.CacheProvider.Simple(), classFileLocator, TypePool.Default.ReaderMode.FAST, TypePool.ClassLoading.ofBootPath()); project.getLogger().info("Processing class files located in in: {}", root); ByteBuddy byteBuddy; try { byteBuddy = entryPoint.getByteBuddy(); } catch (Throwable throwable) { throw new GradleException("Cannot create Byte Buddy instance", throwable); } processDirectory(root, root, byteBuddy, entryPoint, byteBuddyExtension.getMethodNameTransformer(), classFileLocator, typePool, plugins); } finally { classFileLocator.close(); } } /** * Processes a directory. * * @param root The root directory to process. * @param folder The currently processed folder. * @param byteBuddy The Byte Buddy instance to use. * @param entryPoint The transformation's entry point. * @param methodNameTransformer The method name transformer to use. * @param classFileLocator The class file locator to use. * @param typePool The type pool to query for type descriptions. * @param plugins The plugins to apply. */ private void processDirectory(File root, File folder, ByteBuddy byteBuddy, EntryPoint entryPoint, MethodNameTransformer methodNameTransformer, ClassFileLocator classFileLocator, TypePool typePool, List<Plugin> plugins) { File[] file = folder.listFiles(); if (file != null) { for (File aFile : file) { if (aFile.isDirectory()) { processDirectory(root, aFile, byteBuddy, entryPoint, methodNameTransformer, classFileLocator, typePool, plugins); } else if (aFile.isFile() && aFile.getName().endsWith(CLASS_FILE_EXTENSION)) { processClassFile(root, root.toURI().relativize(aFile.toURI()).toString(), byteBuddy, entryPoint, methodNameTransformer, classFileLocator, typePool, plugins); } else { project.getLogger().debug("Skipping ignored file: {}", aFile); } } } } /** * Processes a class file. * * @param root The root directory to process. * @param file The class file to process. * @param byteBuddy The Byte Buddy instance to use. * @param entryPoint The transformation's entry point. * @param methodNameTransformer The method name transformer to use. * @param classFileLocator The class file locator to use. * @param typePool The type pool to query for type descriptions. * @param plugins The plugins to apply. */ private void processClassFile(File root, String file, ByteBuddy byteBuddy, EntryPoint entryPoint, MethodNameTransformer methodNameTransformer, ClassFileLocator classFileLocator, TypePool typePool, List<Plugin> plugins) { String typeName = file.replace('/', '.').substring(0, file.length() - CLASS_FILE_EXTENSION.length()); project.getLogger().debug("Processing class file: {}", typeName); TypeDescription typeDescription = typePool.describe(typeName).resolve(); DynamicType.Builder<?> builder; try { builder = entryPoint.transform(typeDescription, byteBuddy, classFileLocator, methodNameTransformer); } catch (Throwable throwable) { throw new GradleException("Cannot transform type: " + typeName, throwable); } boolean transformed = false; for (Plugin plugin : plugins) { try { if (plugin.matches(typeDescription)) { builder = plugin.apply(builder, typeDescription); transformed = true; } } catch (Throwable throwable) { throw new GradleException("Cannot apply " + plugin + " on " + typeName, throwable); } } if (transformed) { project.getLogger().info("Transformed type: {}", typeName); DynamicType dynamicType = builder.make(); for (Map.Entry<TypeDescription, LoadedTypeInitializer> entry : dynamicType.getLoadedTypeInitializers().entrySet()) { if (byteBuddyExtension.isFailOnLiveInitializer() && entry.getValue().isAlive()) { throw new GradleException("Cannot apply live initializer for " + entry.getKey()); } } try { dynamicType.saveIn(root); } catch (IOException exception) { throw new GradleException("Cannot save " + typeName + " in " + root, exception); } } else { project.getLogger().debug("Skipping non-transformed type: {}", typeName); } } }