/*
* Copyright 2016 the original author or authors.
*
* 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 org.gradle;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileVisitDetails;
import org.gradle.api.file.FileVisitor;
import org.gradle.api.tasks.*;
import org.gradle.internal.exceptions.Contextual;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.commons.ClassRemapper;
import org.objectweb.asm.commons.Remapper;
import com.google.common.io.ByteStreams;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
import java.util.jar.JarFile;
import java.io.*;
import java.util.*;
@CacheableTask
public class ShadedJar extends DefaultTask {
private FileCollection sourceFiles;
private File classesDir;
private File jarFile;
private File analysisFile;
private String shadowPackage;
private Set<String> keepPackages = new LinkedHashSet<>();
private Set<String> unshadedPackages = new LinkedHashSet<>();
private Set<String> ignorePackages = new LinkedHashSet<>();
/**
* The directory to write temporary class files to.
*/
@OutputDirectory
public File getClassesDir() {
return classesDir;
}
public void setClassesDir(File classesDir) {
this.classesDir = classesDir;
}
/**
* The output Jar file.
*/
@OutputFile
public File getJarFile() {
return jarFile;
}
public void setJarFile(File jarFile) {
this.jarFile = jarFile;
}
/**
* The package name to prefix all shaded class names with.
*/
@Input
public String getShadowPackage() {
return shadowPackage;
}
public void setShadowPackage(String shadowPackage) {
this.shadowPackage = shadowPackage;
}
/**
* Retain only those classes in the keep package hierarchies, plus any classes that are reachable from these classes.
*/
@Input
public Set<String> getKeepPackages() {
return keepPackages;
}
public void setKeepPackages(Set<String> keepPackages) {
this.keepPackages = keepPackages;
}
/**
* Do not rename classes in the unshaded package hierarchies. Always includes 'java'.
*/
@Input
public Set<String> getUnshadedPackages() {
return unshadedPackages;
}
public void setUnshadedPackages(Set<String> unshadedPackages) {
this.unshadedPackages = unshadedPackages;
}
/**
* Do not retain classes in the ingore packages hierarchies, unless reachable from some other retained class.
*/
@Input
public Set<String> getIgnorePackages() {
return ignorePackages;
}
public void setIgnorePackages(Set<String> ignorePackages) {
this.ignorePackages = ignorePackages;
}
/**
* The source files to generate the jar from.
*/
@Classpath
public FileCollection getSourceFiles() {
return sourceFiles;
}
public void setSourceFiles(FileCollection sourceFiles) {
this.sourceFiles = sourceFiles;
}
/**
* File to write the text analysis report to.
*/
@OutputFile
public File getAnalysisFile() {
return analysisFile;
}
public void setAnalysisFile(File analysisFile) {
this.analysisFile = analysisFile;
}
@TaskAction
public void run() throws Exception {
long start = System.currentTimeMillis();
PrintWriter writer = new PrintWriter(analysisFile);
try {
final ClassGraph classes = new ClassGraph(new PackagePatterns(keepPackages), new PackagePatterns(unshadedPackages), new PackagePatterns(ignorePackages), shadowPackage);
List<FileCollection> classFiles = new ArrayList<>();
for (File sourceFile : sourceFiles) {
classFiles.add(getProject().zipTree(sourceFile));
}
analyse(getProject().files(classFiles), classes, writer);
writeJar(classes, classesDir, jarFile, writer);
} finally {
writer.close();
}
long end = System.currentTimeMillis();
System.out.println("Analysis took " + (end-start) + "ms.");
}
private void analyse(FileCollection sourceFiles, final ClassGraph classes, final PrintWriter writer) {
final PackagePatterns ignored = new PackagePatterns(Collections.singleton("java"));
sourceFiles.getAsFileTree().visit(new FileVisitor() {
boolean seenManifest;
public void visitDir(FileVisitDetails dirDetails) {
}
public void visitFile(FileVisitDetails fileDetails) {
writer.print(fileDetails.getPath() + ": ");
if (fileDetails.getPath().endsWith(".class")) {
try {
ClassReader reader;
try (InputStream inputStream = new BufferedInputStream(fileDetails.open())) {
reader = new ClassReader(inputStream);
}
final ClassDetails details = classes.get(reader.getClassName());
details.visited = true;
ClassWriter classWriter = new ClassWriter(0);
reader.accept(new ClassRemapper(classWriter, new Remapper() {
public String map(String name) {
if (ignored.matches(name)) {
return name;
}
ClassDetails dependencyDetails = classes.get(name);
if (dependencyDetails != details) {
details.dependencies.add(dependencyDetails);
}
return dependencyDetails.outputClassName;
}
}), ClassReader.EXPAND_FRAMES);
writer.println("mapped class name: " + details.outputClassName);
File outputFile = new File(classesDir, details.outputClassName.concat(".class"));
outputFile.getParentFile().mkdirs();
try (OutputStream outputStream = new FileOutputStream(outputFile)) {
outputStream.write(classWriter.toByteArray());
}
} catch (Exception exception) {
throw new ClassAnalysisException("Could not transform class from " + fileDetails.getFile(), exception);
}
} else if (fileDetails.getPath().endsWith(".properties") && classes.unshadedPackages.matches(fileDetails.getPath())) {
writer.println("include");
classes.addResource(new ResourceDetails(fileDetails.getPath(), fileDetails.getFile()));
} else if (fileDetails.getPath().equals(JarFile.MANIFEST_NAME) && !seenManifest) {
seenManifest = true;
classes.manifest = new ResourceDetails(fileDetails.getPath(), fileDetails.getFile());
} else {
writer.println("skipped");
}
}
});
}
private void writeJar(ClassGraph classes, File classesDir, File jarFile, PrintWriter writer) {
try {
writer.println();
writer.println("CLASS GRAPH");
writer.println();
try (OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(jarFile))) {
JarOutputStream jarOutputStream = new JarOutputStream(outputStream);
if (classes.manifest != null) {
addJarEntry(classes.manifest.resourceName, classes.manifest.sourceFile, jarOutputStream);
}
Set<ClassDetails> visited = new LinkedHashSet<>();
for (ClassDetails classDetails : classes.entryPoints) {
visitTree(classDetails, classesDir, jarOutputStream, writer, "- ", visited);
}
for (ResourceDetails resource : classes.resources) {
addJarEntry(resource.resourceName, resource.sourceFile, jarOutputStream);
}
jarOutputStream.close();
}
} catch (Exception exception) {
throw new ClassAnalysisException("Could not write shaded Jar " + jarFile, exception);
}
}
private void visitTree(ClassDetails classDetails, File classesDir, JarOutputStream jarOutputStream, PrintWriter writer, String prefix, Set<ClassDetails> visited) throws IOException {
if (!visited.add(classDetails)) {
return;
}
if (classDetails.visited) {
writer.println(prefix + classDetails.className);
String fileName = classDetails.outputClassName.concat(".class");
File classFile = new File(classesDir, fileName);
addJarEntry(fileName, classFile, jarOutputStream);
for (ClassDetails dependency : classDetails.dependencies) {
String childPrefix = " " + prefix;
visitTree(dependency, classesDir, jarOutputStream, writer, childPrefix, visited);
}
} else {
writer.println(prefix + classDetails.className + " (not included)");
}
}
private void addJarEntry(String entryName, File sourceFile, JarOutputStream jarOutputStream) throws IOException {
jarOutputStream.putNextEntry(new ZipEntry(entryName));
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(sourceFile))) {
ByteStreams.copy(inputStream, jarOutputStream);
}
jarOutputStream.closeEntry();
}
private static class ClassGraph {
final Map<String, ClassDetails> classes = new LinkedHashMap<>();
final Set<ClassDetails> entryPoints = new LinkedHashSet<>();
final Set<ResourceDetails> resources = new LinkedHashSet<>();
ResourceDetails manifest;
final PackagePatterns unshadedPackages;
final PackagePatterns ignorePackages;
final PackagePatterns keepPackages;
final String shadowPackagePrefix;
public ClassGraph(PackagePatterns keepPackages, PackagePatterns unshadedPackages, PackagePatterns ignorePackages, String shadowPackage) {
this.keepPackages = keepPackages;
this.unshadedPackages = unshadedPackages;
this.ignorePackages = ignorePackages;
this.shadowPackagePrefix = shadowPackage.replace('.', '/').concat("/");
}
public void addResource(ResourceDetails resource) {
resources.add(resource);
}
public ClassDetails get(String className) {
ClassDetails classDetails = classes.get(className);
if (classDetails == null) {
classDetails = new ClassDetails(className, unshadedPackages.matches(className) ? className : shadowPackagePrefix + className);
classes.put(className, classDetails);
if (keepPackages.matches(className) && !ignorePackages.matches(className)) {
entryPoints.add(classDetails);
}
}
return classDetails;
}
}
private static class ResourceDetails {
final String resourceName;
final File sourceFile;
public ResourceDetails(String resourceName, File sourceFile) {
this.resourceName = resourceName;
this.sourceFile = sourceFile;
}
}
private static class ClassDetails {
final String className;
final String outputClassName;
boolean visited;
final Set<ClassDetails> dependencies = new LinkedHashSet<>();
public ClassDetails(String className, String outputClassName) {
this.className = className;
this.outputClassName = outputClassName;
}
}
private static class PackagePatterns {
private final Set<String> prefixes = new HashSet<>();
private final Set<String> names = new HashSet<>();
public PackagePatterns(Set<String> prefixes) {
for (String prefix : prefixes) {
String internalName = prefix.replace('.', '/');
this.names.add(internalName);
this.prefixes.add(internalName + "/");
}
}
public boolean matches(String packageName) {
if (names.contains(packageName)) {
return true;
}
for (String prefix : prefixes) {
if (packageName.startsWith(prefix)) {
names.add(packageName);
return true;
}
}
return false;
}
}
@Contextual
public static class ClassAnalysisException extends RuntimeException {
public ClassAnalysisException(String message, Throwable cause) {
super(message, cause);
}
}
}