package org.netbeans.gradle.build;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.apache.maven.shared.dependency.analyzer.asm.ASMDependencyAnalyzer;
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ResolvedArtifact;
import org.gradle.api.artifacts.ResolvedDependency;
import org.gradle.api.file.FileCollection;
import org.gradle.jvm.tasks.Jar;
public final class NbmDependencyVerifierPlugin implements Plugin<Project> {
@Override
public void apply(final Project project) {
Task verifyTask = project.task("verifyModuleDependencies");
verifyTask.dependsOn("jar");
project.getTasks().getByName("check").dependsOn(verifyTask);
verifyTask.doLast(new Action<Task>() {
@Override
public void execute(Task task) {
Jar jar = (Jar)project.getTasks().getByName("jar");
verifyDependencies(project, jar.getArchivePath());
}
});
}
private void verifyDependencies(Project project, File jarPath) {
try {
verifyDependencies0(project, jarPath);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
private static Set<String> getOwnClasses(File jarPath) throws IOException {
final Set<String> result = new HashSet<>();
try (FileSystem fs = FileSystems.newFileSystem(jarPath.toPath(), null)) {
for (Path root: fs.getRootDirectories()) {
Files.walkFileTree(root, new FileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String extension = ".class";
String path = file.toString();
if (path.toLowerCase(Locale.ROOT).endsWith(extension)) {
String extensionLessPath = path.substring(0, path.length() - extension.length());
String className = removeStartingChars(extensionLessPath.replace('/', '.'), '.');
result.add(className);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
});
}
}
return result;
}
private static String removeStartingChars(String str, char ch) {
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) != ch) {
return str.substring(i);
}
}
return "";
}
private void verifyDependencies0(Project project, File jarPath) throws IOException {
Set<String> ownClasses = getOwnClasses(jarPath);
Set<String> classes = getClasses(jarPath);
Configuration providedCompile = project.getConfigurations().getByName("providedCompile");
FileCollection compile = project.getConfigurations().getByName("compile").minus(providedCompile);
Map<String, String> missing = new HashMap<>();
try (URLClassLoader accessibleLoader = urlClassLoader(getFirstLevelUrls(providedCompile));
URLClassLoader allClassLoader = urlClassLoader(getAllUrls(providedCompile));
URLClassLoader compileLoader = urlClassLoader(getAllUrls(compile))) {
for (String className: classes) {
if (!ownClasses.contains(className) && !hasClass(accessibleLoader, className)) {
if (hasClass(allClassLoader, className) && !hasClass(compileLoader, className)) {
URL owner = tryGetClassOwner(allClassLoader, className);
missing.put(className, owner != null ? owner.toString() : "unknown");
}
}
}
}
if (!missing.isEmpty()) {
failWithMissingDependency(missing);
}
}
private static URLClassLoader urlClassLoader(List<URL> urls) {
return new URLClassLoader(urls.toArray(new URL[urls.size()]), ClassLoader.getSystemClassLoader());
}
private static URL tryGetClassOwner(ClassLoader loader, String className) {
Class<?> cl;
try {
cl = loader.loadClass(className);
} catch (ClassNotFoundException ex) {
return null;
}
CodeSource codeSource = cl.getProtectionDomain().getCodeSource();
if (codeSource == null) {
return null;
}
return codeSource.getLocation();
}
private static boolean hasClass(ClassLoader loader, String className) {
return loader.getResource(className.replace('.', '/') + ".class") != null;
}
private void failWithMissingDependency(Map<String, String> missing) {
List<String> sortedMissing = new ArrayList<>(missing.keySet());
Collections.sort(sortedMissing);
StringBuilder message = new StringBuilder();
message.append("The following classes are not in a directly declared dependency:");
for (String dependency: sortedMissing) {
message.append("\n- ");
message.append(dependency);
message.append(" but was found in ");
message.append(missing.get(dependency));
}
message.append("\n\nRequired explicit dependencies:");
Set<String> result = new TreeSet<>(missing.values());
for (String owner: result) {
message.append("\n- ");
message.append(owner);
}
message.append("\n");
throw new IllegalStateException(message.toString());
}
private List<URL> getAllUrls(FileCollection config) throws MalformedURLException {
List<URL> result = new ArrayList<>();
for (File dep: config.getFiles()) {
result.add(dep.toURI().toURL());
}
return result;
}
private List<URL> getFirstLevelUrls(Configuration config) throws MalformedURLException {
List<URL> result = new ArrayList<>();
for (ResolvedDependency dep: config.getResolvedConfiguration().getFirstLevelModuleDependencies()) {
for (ResolvedArtifact art: dep.getModuleArtifacts()) {
result.add(art.getFile().toURI().toURL());
}
}
return result;
}
private Set<String> getClasses(File jarPath) throws IOException {
ASMDependencyAnalyzer analizer = new ASMDependencyAnalyzer();
return analizer.analyze(jarPath.toURI().toURL());
}
}