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.Arrays; import java.util.Collection; 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.Objects; 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.Dependency; import org.gradle.api.artifacts.ResolvedArtifact; import org.gradle.api.artifacts.ResolvedDependency; import org.gradle.api.artifacts.component.ComponentIdentifier; import org.gradle.api.artifacts.component.ModuleComponentIdentifier; import org.gradle.api.artifacts.dsl.DependencyHandler; import org.gradle.api.file.FileCollection; import org.gradle.jvm.tasks.Jar; public final class NbmDependencyVerifierPlugin implements Plugin<Project> { private static final List<VersionlessArtifactId> UNCHECKED = Arrays.asList( new VersionlessArtifactId("com.github.kelemen", "netbeans-gradle-default-models")); @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"); Configuration uncheckedDependencies = getUncheckedDependencies(project, providedCompile); FileCollection compile = project.getConfigurations().getByName("compile") .minus(providedCompile) .plus(uncheckedDependencies); 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 boolean contains(ModuleComponentIdentifier dependency, Collection<VersionlessArtifactId> collection) { VersionlessArtifactId searched = new VersionlessArtifactId(dependency.getGroup(), dependency.getModule()); return collection.contains(searched); } private static Configuration getUncheckedDependencies(Project project, Configuration providedCompile) { List<ModuleComponentIdentifier> resultIds = new ArrayList<>(); for (ResolvedArtifact dependency : providedCompile.getResolvedConfiguration().getResolvedArtifacts()) { ModuleComponentIdentifier id = tryGetId(dependency); if (id != null) { if (contains(id, UNCHECKED)) { resultIds.add(id); } } } DependencyHandler dependencies = project.getDependencies(); List<Dependency> result = new ArrayList<>(resultIds.size()); for (ModuleComponentIdentifier id : resultIds) { Dependency dependency = dependencies.module(id.getGroup() + ":" + id.getModule() + ":" + id.getVersion()); result.add(dependency); } return project.getConfigurations().detachedConfiguration(result.toArray(new Dependency[0])); } private static ModuleComponentIdentifier tryGetId(ResolvedArtifact dependency) { ComponentIdentifier id = dependency.getId().getComponentIdentifier(); return id instanceof ModuleComponentIdentifier ? (ModuleComponentIdentifier) id : null; } 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()); } private static final class VersionlessArtifactId { private final String group; private final String name; public VersionlessArtifactId(String group, String name) { this.group = group; this.name = name; } @Override public int hashCode() { int hash = 5; hash = 89 * hash + Objects.hashCode(group); hash = 89 * hash + Objects.hashCode(name); return hash; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; final VersionlessArtifactId other = (VersionlessArtifactId) obj; return Objects.equals(this.group, other.group) && Objects.equals(this.name, other.name); } } }