|
| 1 | +package org.apache.logging.log4j.transform.gradle |
| 2 | + |
| 3 | +import org.apache.logging.log4j.weaver.Constants |
| 4 | +import org.gradle.api.DefaultTask |
| 5 | +import org.gradle.api.file.RegularFileProperty |
| 6 | +import org.gradle.api.provider.Property |
| 7 | +import org.gradle.api.provider.SetProperty |
| 8 | +import org.gradle.api.tasks.Internal |
| 9 | +import org.gradle.api.tasks.TaskAction |
| 10 | +import org.gradle.api.tasks.Input |
| 11 | +import org.gradle.api.tasks.InputDirectory |
| 12 | +import org.gradle.api.tasks.OutputDirectory |
| 13 | +import org.apache.logging.log4j.weaver.LocationClassConverter |
| 14 | +import org.apache.logging.log4j.weaver.LocationCacheGenerator |
| 15 | +import org.codehaus.plexus.util.DirectoryScanner |
| 16 | +import java.nio.file.Files |
| 17 | +import java.nio.file.Path |
| 18 | + |
| 19 | +/** |
| 20 | + * A Gradle task for weaving Log4j transformations into compiled class files using the log4j-weaver library. |
| 21 | + * This task mimics the functionality of the log4j-transform-maven-plugin's process-classes goal. |
| 22 | + */ |
| 23 | +abstract class Log4jWeaverTask extends DefaultTask { |
| 24 | + /** |
| 25 | + * The directory containing the compiled source class files to process. |
| 26 | + */ |
| 27 | + @InputDirectory |
| 28 | + abstract Property<String> getSourceDirectoryPath() |
| 29 | + |
| 30 | + /** |
| 31 | + * The directory where transformed class files will be written (typically the same as sourceDirectory for in-place transformation). |
| 32 | + */ |
| 33 | + @OutputDirectory |
| 34 | + abstract Property<File> getOutputDirectory() |
| 35 | + |
| 36 | + /** |
| 37 | + * Tolerance in milliseconds for determining if a class file needs processing based on timestamps. |
| 38 | + */ |
| 39 | + @Input |
| 40 | + abstract Property<Long> getToleranceMillis() |
| 41 | + |
| 42 | + /** |
| 43 | + * Set of include patterns for class files |
| 44 | + */ |
| 45 | + @Input |
| 46 | + abstract SetProperty<String> includes = project.objects.setProperty(String) |
| 47 | + |
| 48 | + /** |
| 49 | + * Set of exclude patterns for class files. |
| 50 | + */ |
| 51 | + @Input |
| 52 | + abstract SetProperty<String> excludes = project.objects.setProperty(String) |
| 53 | + |
| 54 | + @Internal |
| 55 | + File sourceDirectory |
| 56 | + |
| 57 | + /** |
| 58 | + * The main action of the task: weaves Log4j transformations into class files. |
| 59 | + */ |
| 60 | + @TaskAction |
| 61 | + void weave() { |
| 62 | + sourceDirectory = project.file(sourceDirectoryPath.get()) |
| 63 | + logger.info("Starting Log4jWeaverTask: sourceDir=$sourceDirectory, outputDir=$outputDirectory, includes=$includes, excludes=$excludes") |
| 64 | + if (!sourceDirectory.exists()) { |
| 65 | + logger.warn("Skipping task: source directory ${sourceDirectory} does not exist") |
| 66 | + return |
| 67 | + } |
| 68 | + |
| 69 | + URLClassLoader classLoader = createClassLoader() |
| 70 | + LocationCacheGenerator locationCache = new LocationCacheGenerator() |
| 71 | + LocationClassConverter converter = new LocationClassConverter(classLoader) |
| 72 | + |
| 73 | + try { |
| 74 | + Set<Path> filesToProcess = getFilesToProcess(sourceDirectory.toPath(), outputDirectory.get().toPath()) |
| 75 | + if (filesToProcess.empty) { |
| 76 | + logger.warn("No class files selected for transformation") |
| 77 | + return |
| 78 | + } |
| 79 | + |
| 80 | + filesToProcess.groupBy { Path path -> LocationCacheGenerator.getCacheClassFile(path) } |
| 81 | + .values() |
| 82 | + .each { List<Path> classFiles -> |
| 83 | + convertClassFiles(classFiles, converter, locationCache) |
| 84 | + } |
| 85 | + |
| 86 | + Map<String, byte[]> cacheClasses = locationCache.generateClasses() |
| 87 | + cacheClasses.each { String className, byte[] data -> |
| 88 | + saveCacheFile(className, data) |
| 89 | + } |
| 90 | + } catch (Exception e) { |
| 91 | + logger.error("Failed to process class files", e) |
| 92 | + throw new RuntimeException("Failed to process class files", e) |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + /** |
| 97 | + * Creates a ClassLoader including the source directory and runtime classpath dependencies. |
| 98 | + * |
| 99 | + * @return The created URLClassLoader. |
| 100 | + */ |
| 101 | + private URLClassLoader createClassLoader() { |
| 102 | + try { |
| 103 | + List<URL> urls = [] |
| 104 | + urls << sourceDirectory.toURI().toURL() |
| 105 | + project.configurations.runtimeClasspath.files.each { File file -> |
| 106 | + urls << file.toURI().toURL() |
| 107 | + } |
| 108 | + URLClassLoader classLoader = new URLClassLoader(urls as URL[], Thread.currentThread().contextClassLoader) |
| 109 | + return classLoader |
| 110 | + } catch (Exception e) { |
| 111 | + logger.error("Failed to create ClassLoader", e) |
| 112 | + throw new RuntimeException("Failed to create ClassLoader", e) |
| 113 | + } |
| 114 | + } |
| 115 | + |
| 116 | + /** |
| 117 | + * Scans the source directory for class files that need processing based on include/exclude patterns and timestamp checks. |
| 118 | + * |
| 119 | + * @param sourceDir The source directory path. |
| 120 | + * @param outputDir The output directory path. |
| 121 | + * @return Set of Path objects for class files that need processing. |
| 122 | + */ |
| 123 | + private Set<Path> getFilesToProcess(Path sourceDir, Path outputDir) { |
| 124 | + DirectoryScanner scanner = new DirectoryScanner() |
| 125 | + scanner.setBasedir(sourceDir.toFile()) |
| 126 | + scanner.setIncludes(includes.get() as String[]) |
| 127 | + scanner.setExcludes(excludes.get() as String[]) |
| 128 | + scanner.scan() |
| 129 | + |
| 130 | + String[] includedFiles = scanner.getIncludedFiles() |
| 131 | + |
| 132 | + Set<Path> filesToProcess = includedFiles.findAll { String relativePath -> |
| 133 | + Path outputPath = outputDir.resolve(relativePath) |
| 134 | + return !Files.exists(outputPath) || |
| 135 | + Files.getLastModifiedTime(sourceDir.resolve(relativePath)).toMillis() + toleranceMillis.get() > |
| 136 | + Files.getLastModifiedTime(outputPath).toMillis() |
| 137 | + }.collect { String relativePath -> |
| 138 | + sourceDir.resolve(relativePath) |
| 139 | + }.toSet() |
| 140 | + |
| 141 | + return filesToProcess |
| 142 | + } |
| 143 | + |
| 144 | + /** |
| 145 | + * Converts a group of class files using the LocationClassConverter. |
| 146 | + * |
| 147 | + * @param classFiles List of class file paths to convert. |
| 148 | + * @param converter The LocationClassConverter to use for transformation. |
| 149 | + * @param locationCache The LocationCacheGenerator for cache management. |
| 150 | + */ |
| 151 | + protected void convertClassFiles(List<Path> classFiles, LocationClassConverter converter, LocationCacheGenerator locationCache) { |
| 152 | + Path sourceDir = sourceDirectory.toPath() |
| 153 | + ByteArrayOutputStream buf = new ByteArrayOutputStream() |
| 154 | + classFiles.sort() |
| 155 | + classFiles.each { Path classFile -> |
| 156 | + try { |
| 157 | + buf.reset() |
| 158 | + Files.newInputStream(sourceDir.resolve(classFile)).withCloseable { InputStream src -> |
| 159 | + converter.convert(src, buf, locationCache) |
| 160 | + } |
| 161 | + byte[] data = buf.toByteArray() |
| 162 | + saveClassFile(classFile, data) |
| 163 | + } catch (IOException e) { |
| 164 | + logger.error("Failed to process class file: ${sourceDir.relativize(classFile)}", e) |
| 165 | + throw new RuntimeException("Failed to process class file: ${sourceDir.relativize(classFile)}", e) |
| 166 | + } |
| 167 | + } |
| 168 | + } |
| 169 | + |
| 170 | + /** |
| 171 | + * Saves a transformed class file to the output directory. |
| 172 | + * |
| 173 | + * @param dest The relative path of the class file to save. |
| 174 | + * @param data The byte array of the transformed class file. |
| 175 | + */ |
| 176 | + protected void saveClassFile(Path dest, byte[] data) { |
| 177 | + Path outputPath = outputDirectory.get().toPath().resolve(dest) |
| 178 | + saveFile(outputPath, data) |
| 179 | + logger.info("Saved transformed class file: ${outputDirectory.get().toPath().relativize(outputPath)}") |
| 180 | + } |
| 181 | + |
| 182 | + /** |
| 183 | + * Saves a generated cache class file to the output directory. |
| 184 | + * |
| 185 | + * @param internalClassName The internal name of the class (e.g., 'org/apache/logging/log4j/some/CacheClass'). |
| 186 | + * @param data The byte array of the cache class file. |
| 187 | + */ |
| 188 | + protected void saveCacheFile(String internalClassName, byte[] data) { |
| 189 | + Path outputPath = outputDirectory.get().toPath().resolve("${internalClassName}.class") |
| 190 | + saveFile(outputPath, data) |
| 191 | + logger.info("Saved cache class file: ${outputDirectory.get().toPath().relativize(outputPath)}") |
| 192 | + } |
| 193 | + |
| 194 | + protected static void saveFile(Path outputPath, byte[] data) { |
| 195 | + Files.createDirectories(outputPath.parent) |
| 196 | + Files.write(outputPath, data) |
| 197 | + } |
| 198 | +} |
0 commit comments