From 57296b76c4b610461bfebc41214b0e9134082c27 Mon Sep 17 00:00:00 2001 From: Alexandre Fillatre Date: Sun, 27 Mar 2022 22:13:44 +0200 Subject: [PATCH] Move logic in the core, so it's independent from a specific dependency manager --- .../kth/depclean/core/AbstractDebloater.java | 73 +++++ .../se/kth/depclean/core/DepCleanManager.java | 211 +++++++++++++ .../analysis/AnalysisFailureException.java | 16 + .../ProjectDependencyAnalysisBuilder.java | 13 +- .../java/se/kth/depclean/DepCleanMojo.java | 289 ++---------------- ...atedPomWriter.java => MavenDebloater.java} | 112 +++---- .../util/json/ParsedDependencies.java | 4 +- .../wrapper/MavenDependencyManager.java | 217 +++++++++++++ 8 files changed, 589 insertions(+), 346 deletions(-) create mode 100644 depclean-core/src/main/java/se/kth/depclean/core/AbstractDebloater.java create mode 100644 depclean-core/src/main/java/se/kth/depclean/core/DepCleanManager.java create mode 100644 depclean-core/src/main/java/se/kth/depclean/core/analysis/AnalysisFailureException.java rename depclean-maven-plugin/src/main/java/se/kth/depclean/util/{DebloatedPomWriter.java => MavenDebloater.java} (57%) create mode 100644 depclean-maven-plugin/src/main/java/se/kth/depclean/wrapper/MavenDependencyManager.java diff --git a/depclean-core/src/main/java/se/kth/depclean/core/AbstractDebloater.java b/depclean-core/src/main/java/se/kth/depclean/core/AbstractDebloater.java new file mode 100644 index 00000000..ea017a8d --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/AbstractDebloater.java @@ -0,0 +1,73 @@ +package se.kth.depclean.core; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import se.kth.depclean.core.analysis.model.DebloatedDependency; +import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis; + +/** + * Analyses the analysis result and writes the debloated config file. + */ +@Slf4j +@AllArgsConstructor +public abstract class AbstractDebloater { + + protected final ProjectDependencyAnalysis analysis; + + /** + * Writes the debloated config file down. + */ + public void write() throws IOException { + log.info("Starting debloating file"); + logChanges(); + setDependencies(analysis.getDebloatedDependencies().stream() + .map(this::toMavenDependency) + .collect(Collectors.toList())); + + if (log.isDebugEnabled()) { + logDependencies(); + } + postProcessDependencies(); + writeFile(); + } + + protected abstract T toMavenDependency(DebloatedDependency debloatedDependency); + + protected abstract void setDependencies(List dependencies); + + protected abstract void writeFile() throws IOException; + + protected abstract void logDependencies(); + + /** + * In order to keep the version as variable (property) for dependencies that were declared as such, post-process + * dependencies to replace interpolated version with the initial one. + */ + protected abstract void postProcessDependencies(); + + private void logChanges() { + if (analysis.hasUsedTransitiveDependencies()) { + final int dependencyAmount = analysis.getUsedTransitiveDependencies().size(); + log.info("Adding {} used transitive {} as direct {}.", + dependencyAmount, getDependencyWording(dependencyAmount), getDependencyWording(dependencyAmount)); + } + + if (analysis.hasUnusedDirectDependencies()) { + final int dependencyAmount = analysis.getUnusedDirectDependencies().size(); + log.info("Removing {} unused direct {}.", dependencyAmount, getDependencyWording(dependencyAmount)); + } + + if (analysis.hasUnusedTransitiveDependencies()) { + final int dependencyAmount = analysis.getUnusedTransitiveDependencies().size(); + log.info( + "Excluding {} unused transitive {} one-by-one.", dependencyAmount, getDependencyWording(dependencyAmount)); + } + } + + private String getDependencyWording(int amount) { + return amount > 1 ? "dependencies" : "dependency"; + } +} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/DepCleanManager.java b/depclean-core/src/main/java/se/kth/depclean/core/DepCleanManager.java new file mode 100644 index 00000000..5d576dbd --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/DepCleanManager.java @@ -0,0 +1,211 @@ +package se.kth.depclean.core; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import org.apache.commons.io.FileUtils; +import org.apache.maven.plugin.logging.Log; +import org.jetbrains.annotations.Nullable; +import se.kth.depclean.core.analysis.AnalysisFailureException; +import se.kth.depclean.core.analysis.DefaultProjectDependencyAnalyzer; +import se.kth.depclean.core.analysis.ProjectDependencyAnalyzerException; +import se.kth.depclean.core.analysis.graph.DependencyGraph; +import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis; +import se.kth.depclean.core.model.ClassName; +import se.kth.depclean.core.model.Dependency; +import se.kth.depclean.core.model.ProjectContext; +import se.kth.depclean.core.model.Scope; +import se.kth.depclean.core.wrapper.DependencyManagerWrapper; + +/** + * Runs the depclean process, regardless of a specific dependency manager. + */ +@AllArgsConstructor +public class DepCleanManager { + + private static final String SEPARATOR = "-------------------------------------------------------"; + + private final DependencyManagerWrapper dependencyManager; + private final boolean skipDepClean; + private final boolean ignoreTests; + private final Set ignoreScopes; + private final Set ignoreDependencies; + private final boolean failIfUnusedDirect; + private final boolean failIfUnusedTransitive; + private final boolean failIfUnusedInherited; + private final boolean createPomDebloated; + private final boolean createResultJson; + private final boolean createClassUsageCsv; + + /** + * Execute the depClean manager. + */ + @SneakyThrows + public void execute() throws AnalysisFailureException { + final long startTime = System.currentTimeMillis(); + + if (skipDepClean) { + getLog().info("Skipping DepClean plugin execution"); + return; + } + printString(SEPARATOR); + getLog().info("Starting DepClean dependency analysis"); + + if (dependencyManager.isMaven() && dependencyManager.isPackagingPom()) { + getLog().info("Skipping because packaging type pom."); + return; + } + + dependencyManager.copyAndExtractDependencies(); + + final ProjectDependencyAnalysis analysis = getAnalysis(); + if (analysis == null) { + return; + } + analysis.print(); + + /* Fail the build if there are unused direct dependencies */ + if (failIfUnusedDirect && analysis.hasUnusedDirectDependencies()) { + throw new AnalysisFailureException( + "Build failed due to unused direct dependencies in the dependency tree of the project."); + } + + /* Fail the build if there are unused transitive dependencies */ + if (failIfUnusedTransitive && analysis.hasUnusedTransitiveDependencies()) { + throw new AnalysisFailureException( + "Build failed due to unused transitive dependencies in the dependency tree of the project."); + } + + /* Fail the build if there are unused inherited dependencies */ + if (failIfUnusedInherited && analysis.hasUnusedInheritedDependencies()) { + throw new AnalysisFailureException( + "Build failed due to unused inherited dependencies in the dependency tree of the project."); + } + + /* Writing the debloated version of the pom */ + if (createPomDebloated) { + dependencyManager.getDebloater(analysis).write(); + } + + /* Writing the JSON file with the debloat results */ + if (createResultJson) { + createResultJson(analysis); + } + + final long stopTime = System.currentTimeMillis(); + getLog().info("Analysis done in " + getTime(stopTime - startTime)); + } + + private void createResultJson(ProjectDependencyAnalysis analysis) { + printString("Creating depclean-results.json, please wait..."); + final File jsonFile = new File(dependencyManager.getBuildDirectory() + File.separator + "depclean-results.json"); + final File treeFile = new File(dependencyManager.getBuildDirectory() + File.separator + "tree.txt"); + final File classUsageFile = new File(dependencyManager.getBuildDirectory() + File.separator + "class-usage.csv"); + try { + dependencyManager.generateDependencyTree(treeFile); + } catch (IOException | InterruptedException e) { + getLog().error("Unable to generate dependency tree."); + // Restore interrupted state... + Thread.currentThread().interrupt(); + return; + } + if (createClassUsageCsv) { + printString("Creating class-usage.csv, please wait..."); + try { + FileUtils.write(classUsageFile, "OriginClass,TargetClass,Dependency\n", Charset.defaultCharset()); + } catch (IOException e) { + getLog().error("Error writing the CSV header."); + } + } + String treeAsJson = dependencyManager.getTreeAsJson(treeFile, + analysis, + classUsageFile, + createClassUsageCsv + ); + + try { + FileUtils.write(jsonFile, treeAsJson, Charset.defaultCharset()); + } catch (IOException e) { + getLog().error("Unable to generate JSON file."); + } + if (jsonFile.exists()) { + getLog().info("depclean-results.json file created in: " + jsonFile.getAbsolutePath()); + } + if (classUsageFile.exists()) { + getLog().info("class-usage.csv file created in: " + classUsageFile.getAbsolutePath()); + } + } + + @Nullable + private ProjectDependencyAnalysis getAnalysis() { + /* Analyze dependencies usage status */ + final ProjectContext projectContext = buildProjectContext(); + final ProjectDependencyAnalysis analysis; + final DefaultProjectDependencyAnalyzer dependencyAnalyzer = new DefaultProjectDependencyAnalyzer(projectContext); + try { + analysis = dependencyAnalyzer.analyze(); + } catch (ProjectDependencyAnalyzerException e) { + getLog().error("Unable to analyze dependencies."); + return null; + } + return analysis; + } + + private ProjectContext buildProjectContext() { + if (ignoreTests) { + ignoreScopes.add("test"); + } + + final DependencyGraph dependencyGraph = dependencyManager.dependencyGraph(); + return new ProjectContext( + dependencyGraph, + dependencyManager.getOutputDirectory(), + dependencyManager.getTestOutputDirectory(), + ignoreScopes.stream().map(Scope::new).collect(Collectors.toSet()), + toDependency(dependencyGraph.allDependencies(), ignoreDependencies), + dependencyManager.collectUsedClassesFromProcessors().stream().map(ClassName::new).collect(Collectors.toSet()) + ); + } + + /** + * Returns a set of {@code DependencyCoordinate}s that match given string representations. + * + * @param allDependencies all known dependencies + * @param ignoreDependencies string representation of dependencies to return + * @return a set of {@code Dependency} that match given string representations + */ + private Set toDependency(Set allDependencies, Set ignoreDependencies) { + return ignoreDependencies.stream() + .map(dependency -> findDependency(allDependencies, dependency)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + private Dependency findDependency(Set allDependencies, String dependency) { + return allDependencies.stream() + .filter(dep -> dep.toString().toLowerCase().contains(dependency.toLowerCase())) + .findFirst() + .orElse(null); + } + + private String getTime(long millis) { + long minutes = TimeUnit.MILLISECONDS.toMinutes(millis); + long seconds = (TimeUnit.MILLISECONDS.toSeconds(millis) % 60); + + return String.format("%smin %ss", minutes, seconds); + } + + private void printString(final String string) { + System.out.println(string); //NOSONAR avoid a warning of non-used logger + } + + private Log getLog() { + return dependencyManager.getLog(); + } +} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/AnalysisFailureException.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/AnalysisFailureException.java new file mode 100644 index 00000000..2075e146 --- /dev/null +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/AnalysisFailureException.java @@ -0,0 +1,16 @@ +package se.kth.depclean.core.analysis; + +/** + * Indicates the analysis should fail. + */ +public class AnalysisFailureException extends Exception { + + /** + * Create the failure. + * + * @param message the message to explain with the analysis failed + */ + public AnalysisFailureException(String message) { + super(message); + } +} diff --git a/depclean-core/src/main/java/se/kth/depclean/core/analysis/ProjectDependencyAnalysisBuilder.java b/depclean-core/src/main/java/se/kth/depclean/core/analysis/ProjectDependencyAnalysisBuilder.java index 99740a62..309d2789 100644 --- a/depclean-core/src/main/java/se/kth/depclean/core/analysis/ProjectDependencyAnalysisBuilder.java +++ b/depclean-core/src/main/java/se/kth/depclean/core/analysis/ProjectDependencyAnalysisBuilder.java @@ -9,9 +9,10 @@ import java.util.Set; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; -import se.kth.depclean.core.analysis.model.ClassName; -import se.kth.depclean.core.analysis.model.Dependency; -import se.kth.depclean.core.analysis.model.ProjectContext; +import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis; +import se.kth.depclean.core.model.ClassName; +import se.kth.depclean.core.model.Dependency; +import se.kth.depclean.core.model.ProjectContext; /** * Builds the analysis given the declared dependencies and the one actually used. @@ -86,21 +87,21 @@ private Map buildDependencyClassesMap() { private Set getUsedDirectDependencies() { return usedDependencies.stream() .filter(a -> context.getDependencyGraph().directDependencies().contains(a)) - .peek(DependencyCoordinate -> log.trace("## Used Direct dependency {}", DependencyCoordinate)) + .peek(dependency -> log.trace("## Used Direct dependency {}", dependency)) .collect(Collectors.toSet()); } private Set getUsedTransitiveDependencies() { return usedDependencies.stream() .filter(a -> context.getDependencyGraph().transitiveDependencies().contains(a)) - .peek(DependencyCoordinate -> log.trace("## Used Transitive dependency {}", DependencyCoordinate)) + .peek(dependency -> log.trace("## Used Transitive dependency {}", dependency)) .collect(Collectors.toSet()); } private Set getUsedInheritedDependencies() { return usedDependencies.stream() .filter(a -> context.getDependencyGraph().inheritedDependencies().contains(a)) - .peek(DependencyCoordinate -> log.trace("## Used Transitive dependency {}", DependencyCoordinate)) + .peek(dependency -> log.trace("## Used Transitive dependency {}", dependency)) .collect(Collectors.toSet()); } diff --git a/depclean-maven-plugin/src/main/java/se/kth/depclean/DepCleanMojo.java b/depclean-maven-plugin/src/main/java/se/kth/depclean/DepCleanMojo.java index 009de0e3..0c527810 100644 --- a/depclean-maven-plugin/src/main/java/se/kth/depclean/DepCleanMojo.java +++ b/depclean-maven-plugin/src/main/java/se/kth/depclean/DepCleanMojo.java @@ -17,25 +17,10 @@ package se.kth.depclean; -import com.google.common.collect.ImmutableSet; -import fr.dutra.tools.maven.deptree.core.ParseException; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Objects; -import java.util.Optional; import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.FileUtils; import org.apache.maven.execution.MavenSession; -import org.apache.maven.model.Model; -import org.apache.maven.model.io.xpp3.MavenXpp3Reader; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Component; @@ -43,26 +28,11 @@ import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; -import org.apache.maven.project.DefaultProjectBuildingRequest; import org.apache.maven.project.MavenProject; -import org.apache.maven.project.ProjectBuildingRequest; import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder; -import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException; -import org.apache.maven.shared.dependency.graph.DependencyNode; -import org.codehaus.plexus.util.xml.Xpp3Dom; -import se.kth.depclean.core.analysis.DefaultProjectDependencyAnalyzer; -import se.kth.depclean.core.analysis.ProjectDependencyAnalysis; -import se.kth.depclean.core.analysis.ProjectDependencyAnalyzerException; -import se.kth.depclean.core.analysis.graph.DependencyGraph; -import se.kth.depclean.core.analysis.model.ClassName; -import se.kth.depclean.core.analysis.model.Dependency; -import se.kth.depclean.core.analysis.model.ProjectContext; -import se.kth.depclean.core.analysis.model.Scope; -import se.kth.depclean.graph.MavenDependencyGraph; -import se.kth.depclean.util.DebloatedPomWriter; -import se.kth.depclean.util.JarUtils; -import se.kth.depclean.util.MavenInvoker; -import se.kth.depclean.util.json.ParsedDependencies; +import se.kth.depclean.core.DepCleanManager; +import se.kth.depclean.core.analysis.AnalysisFailureException; +import se.kth.depclean.wrapper.MavenDependencyManager; /** * This Maven mojo is the main class of DepClean. DepClean is built on top of the maven-dependency-analyzer component. @@ -77,9 +47,6 @@ @Slf4j public class DepCleanMojo extends AbstractMojo { - private static final String SEPARATOR = "-------------------------------------------------------"; - public static final String DIRECTORY_TO_COPY_DEPENDENCIES = "dependency"; - /** * The Maven project to analyze. */ @@ -169,242 +136,30 @@ public class DepCleanMojo extends AbstractMojo { @Component(hint = "default") private DependencyGraphBuilder dependencyGraphBuilder; - /** - * Returns a list of dependency nodes from a graph of dependency tree. - * - * @param model The maven model - * @return The nodes in the dependency graph. - * @throws DependencyGraphBuilderException if the graph cannot be built. - */ - private DependencyGraph dependencyGraph(Model model) throws DependencyGraphBuilderException { - ProjectBuildingRequest buildingRequest = new DefaultProjectBuildingRequest( - session.getProjectBuildingRequest()); - buildingRequest.setProject(project); - DependencyNode rootNode = dependencyGraphBuilder.buildDependencyGraph(buildingRequest, null); - return new MavenDependencyGraph(project, model, rootNode); - } - - private void printString(final String string) { - System.out.println(string); //NOSONAR avoid a warning of non-used logger - } - @SneakyThrows @Override public final void execute() { - final long startTime = System.currentTimeMillis(); - - if (skipDepClean) { - getLog().info("Skipping DepClean plugin execution"); - return; - } - - printString(SEPARATOR); - getLog().info("Starting DepClean dependency analysis"); - - File pomFile = new File(project.getBasedir().getAbsolutePath() + File.separator + "pom.xml"); - - String packaging = project.getPackaging(); - if (packaging.equals("pom")) { - getLog().info("Skipping because packaging type " + packaging + "."); - return; - } - - /* Build Maven model to manipulate the pom */ - Model model; - FileReader reader; - MavenXpp3Reader mavenReader = new MavenXpp3Reader(); try { - reader = new FileReader(pomFile); - model = mavenReader.read(reader); - model.setPomFile(pomFile); - } catch (Exception ex) { - getLog().error("Unable to build the maven project."); - return; - } - - /* Copy direct dependencies locally */ - try { - MavenInvoker.runCommand("mvn dependency:copy-dependencies -DoutputDirectory=" - + project.getBuild().getDirectory() + File.separator + DIRECTORY_TO_COPY_DEPENDENCIES); - } catch (IOException | InterruptedException e) { - getLog().error("Unable to resolve all the dependencies."); - Thread.currentThread().interrupt(); - return; - } - - // TODO remove this workaround later - if (new File(project.getBuild().getDirectory() + File.separator + "libs").exists()) { - try { - FileUtils.copyDirectory(new File(project.getBuild().getDirectory() + File.separator + "libs"), - new File(project.getBuild().getDirectory() + File.separator + DIRECTORY_TO_COPY_DEPENDENCIES) - ); - } catch (IOException | NullPointerException e) { - getLog().error("Error copying directory libs to dependency"); - } - } - - /* Decompress dependencies */ - String dependencyDirectoryName = - project.getBuild().getDirectory() + "/" + DIRECTORY_TO_COPY_DEPENDENCIES; - File dependencyDirectory = new File(dependencyDirectoryName); - if (dependencyDirectory.exists()) { - JarUtils.decompress(dependencyDirectoryName); - } - - /* Analyze dependencies usage status */ - final ProjectContext projectContext = buildProjectContext(model); - final ProjectDependencyAnalysis analysis; - final DefaultProjectDependencyAnalyzer dependencyAnalyzer = new DefaultProjectDependencyAnalyzer(projectContext); - try { - analysis = dependencyAnalyzer.analyze(); - } catch (ProjectDependencyAnalyzerException e) { - getLog().error("Unable to analyze dependencies."); - return; - } - - analysis.print(); - - /* Fail the build if there are unused direct dependencies */ - if (failIfUnusedDirect && analysis.hasUnusedDirectDependencies()) { - throw new MojoExecutionException( - "Build failed due to unused direct dependencies in the dependency tree of the project."); - } - - /* Fail the build if there are unused transitive dependencies */ - if (failIfUnusedTransitive && analysis.hasUnusedTransitiveDependencies()) { - throw new MojoExecutionException( - "Build failed due to unused transitive dependencies in the dependency tree of the project."); - } - - /* Fail the build if there are unused inherited dependencies */ - if (failIfUnusedInherited && analysis.hasUnusedInheritedDependencies()) { - throw new MojoExecutionException( - "Build failed due to unused inherited dependencies in the dependency tree of the project."); - } - - /* Writing the debloated version of the pom */ - if (createPomDebloated) { - new DebloatedPomWriter(project, model, analysis).write(); - } - - /* Writing the JSON file with the debloat results */ - if (createResultJson) { - printString("Creating depclean-results.json, please wait..."); - final File jsonFile = new File(project.getBuild().getDirectory() + File.separator + "depclean-results.json"); - final File treeFile = new File(project.getBuild().getDirectory() + File.separator + "tree.txt"); - final File classUsageFile = new File(project.getBuild().getDirectory() + File.separator + "class-usage.csv"); - try { - MavenInvoker.runCommand("mvn dependency:tree -DoutputFile=" + treeFile + " -Dverbose=true"); - } catch (IOException | InterruptedException e) { - getLog().error("Unable to generate dependency tree."); - // Restore interrupted state... - Thread.currentThread().interrupt(); - return; - } - if (createClassUsageCsv) { - printString("Creating class-usage.csv, please wait..."); - try { - FileUtils.write(classUsageFile, "OriginClass,TargetClass,Dependency\n", Charset.defaultCharset()); - } catch (IOException e) { - getLog().error("Error writing the CSV header."); - } - } - ParsedDependencies parsedDependencies = new ParsedDependencies( - treeFile, - analysis, - classUsageFile, + new DepCleanManager( + new MavenDependencyManager( + getLog(), + project, + session, + dependencyGraphBuilder + ), + skipDepClean, + ignoreTests, + ignoreScopes, + ignoreDependencies, + failIfUnusedDirect, + failIfUnusedTransitive, + failIfUnusedInherited, + createPomDebloated, + createResultJson, createClassUsageCsv - ); - try { - FileUtils.write(jsonFile, parsedDependencies.parseTreeToJson(), Charset.defaultCharset()); - } catch (ParseException | IOException e) { - getLog().error("Unable to generate JSON file."); - } - if (jsonFile.exists()) { - getLog().info("depclean-results.json file created in: " + jsonFile.getAbsolutePath()); - } - if (classUsageFile.exists()) { - getLog().info("class-usage.csv file created in: " + classUsageFile.getAbsolutePath()); - } - } - - final long stopTime = System.currentTimeMillis(); - log.info("Analysis done in " + getTime(stopTime - startTime)); - } - - private String getTime(long millis) { - long minutes = TimeUnit.MILLISECONDS.toMinutes(millis); - long seconds = (TimeUnit.MILLISECONDS.toSeconds(millis) % 60); - - return String.format("%smin %ss", minutes, seconds); - } - - private ProjectContext buildProjectContext(Model model) throws DependencyGraphBuilderException { - if (ignoreTests) { - ignoreScopes.add("test"); + ).execute(); + } catch (AnalysisFailureException e) { + throw new MojoExecutionException(e.getMessage(), e); } - - final DependencyGraph dependencyGraph = dependencyGraph(model); - return new ProjectContext( - dependencyGraph, - Paths.get(project.getBuild().getOutputDirectory()), - Paths.get(project.getBuild().getTestOutputDirectory()), - ignoreScopes.stream().map(Scope::new).collect(Collectors.toSet()), - toDependencyCoordinates(dependencyGraph.allDependencies(), ignoreDependencies), - collectUsedClassesFromProcessors().stream().map(ClassName::new).collect(Collectors.toSet()) - ); - } - - /** - * Maven processors are defined like this. - *
{@code
-   *       
-   *         org.bsc.maven
-   *         maven-processor-plugin
-   *         
-   *           
-   *             process
-   *             [...]
-   *             
-   *               
-   *                 XXXProcessor
-   *               
-   *             
-   *           
-   *         
-   *       
-   * }
- */ - private Set collectUsedClassesFromProcessors() { - log.trace("# collectUsedClassesFromProcessors()"); - return Optional.ofNullable(project.getPlugin("org.bsc.maven:maven-processor-plugin")) - .map(plugin -> plugin.getExecutionsAsMap().get("process")) - .map(exec -> (Xpp3Dom) exec.getConfiguration()) - .map(config -> config.getChild("processors")) - .map(Xpp3Dom::getChildren) - .map(arr -> Arrays.stream(arr).map(Xpp3Dom::getValue).collect(Collectors.toSet())) - .orElse(ImmutableSet.of()); - } - - /** - * Returns a set of {@code DependencyCoordinate}s that match given string representations. - * - * @param allArtifacts all known artifacts - * @param ignoreDependencies string representation of artificats to return - * @return a set of {@code DependencyCoordinate}s that match given string representations - */ - private Set toDependencyCoordinates(Set allArtifacts, - Set ignoreDependencies) { - return ignoreDependencies.stream() - .map(dependency -> findDependency(allArtifacts, dependency)) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - } - - private Dependency findDependency(Set allArtifacts, String dependency) { - return allArtifacts.stream() - .filter(artifact -> artifact.toString().toLowerCase().contains(dependency.toLowerCase())) - .findFirst() - .orElse(null); } } diff --git a/depclean-maven-plugin/src/main/java/se/kth/depclean/util/DebloatedPomWriter.java b/depclean-maven-plugin/src/main/java/se/kth/depclean/util/MavenDebloater.java similarity index 57% rename from depclean-maven-plugin/src/main/java/se/kth/depclean/util/DebloatedPomWriter.java rename to depclean-maven-plugin/src/main/java/se/kth/depclean/util/MavenDebloater.java index 162ea6c9..c9dc78f8 100644 --- a/depclean-maven-plugin/src/main/java/se/kth/depclean/util/DebloatedPomWriter.java +++ b/depclean-maven-plugin/src/main/java/se/kth/depclean/util/MavenDebloater.java @@ -6,96 +6,71 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; -import java.util.stream.Collectors; -import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.maven.artifact.Artifact; import org.apache.maven.model.Dependency; import org.apache.maven.model.Exclusion; import org.apache.maven.model.Model; import org.apache.maven.model.io.xpp3.MavenXpp3Writer; -import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; -import se.kth.depclean.core.analysis.ProjectDependencyAnalysis; +import se.kth.depclean.core.AbstractDebloater; import se.kth.depclean.core.analysis.model.DebloatedDependency; +import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis; /** * Writes a debloated pom is needed. */ @Slf4j -@AllArgsConstructor -public class DebloatedPomWriter { +public class MavenDebloater extends AbstractDebloater { private final MavenProject project; private final Model model; - private final ProjectDependencyAnalysis analysis; + private final List initialDependencies; /** - * Generates the debloated pom. + * Creates the debloater. * - * @throws MojoExecutionException if file can't be written + * @param analysis the depclean analysis result + * @param project the maven project + * @param model the maven model */ - public void write() throws MojoExecutionException { - log.info("Starting debloating POM"); - logChanges(); - final List initialDependencies = model.getDependencies(); - model.setDependencies(analysis.getDebloatedDependencies().stream() - .map(this::toMavenDependency) - .collect(Collectors.toList())); - - if (log.isDebugEnabled()) { - model.getDependencies().forEach(dep -> { - log.debug("Debloated dependency {}", dep); - dep.getExclusions().forEach(excl -> log.debug("- Excluding {}:{}", - excl.getGroupId(), excl.getArtifactId())); - }); - } - - postProcessDependencies(initialDependencies); - writeDebloatedPom(); - } - - private void logChanges() { - if (analysis.hasUsedTransitiveDependencies()) { - final int dependencyAmount = analysis.getUsedTransitiveDependencies().size(); - log.info("Adding {} used transitive {} as direct {}.", - dependencyAmount, getDependencyWording(dependencyAmount), getDependencyWording(dependencyAmount)); - } - - if (analysis.hasUnusedDirectDependencies()) { - final int dependencyAmount = analysis.getUnusedDirectDependencies().size(); - log.info("Removing {} unused direct {}.", dependencyAmount, getDependencyWording(dependencyAmount)); - } - - if (analysis.hasUnusedTransitiveDependencies()) { - final int dependencyAmount = analysis.getUnusedTransitiveDependencies().size(); - log.info( - "Excluding {} unused transitive {} one-by-one.", dependencyAmount, getDependencyWording(dependencyAmount)); - } + public MavenDebloater(ProjectDependencyAnalysis analysis, MavenProject project, Model model) { + super(analysis); + this.project = project; + this.model = model; + this.initialDependencies = model.getDependencies(); } - private String getDependencyWording(int amount) { - return amount > 1 ? "dependencies" : "dependency"; + @Override + protected void logDependencies() { + model.getDependencies().forEach(dep -> { + log.debug("Debloated dependency {}", dep); + dep.getExclusions().forEach(excl -> log.debug("- Excluding {}:{}", + excl.getGroupId(), excl.getArtifactId())); + }); } - private Dependency toMavenDependency(DebloatedDependency debloatedDependency) { + @Override + protected Dependency toMavenDependency(DebloatedDependency debloatedDependency) { final Dependency dependency = createDependency(debloatedDependency); debloatedDependency.getExclusions().forEach(depToExclude -> exclude(dependency, depToExclude)); return dependency; } - private void exclude(Dependency dependency, se.kth.depclean.core.analysis.model.Dependency dependencyToExclude) { + @Override + protected void setDependencies(List dependencies) { + model.setDependencies(dependencies); + } + + private void exclude(Dependency dependency, se.kth.depclean.core.model.Dependency dependencyToExclude) { Exclusion exclusion = new Exclusion(); exclusion.setGroupId(dependencyToExclude.getGroupId()); exclusion.setArtifactId(dependencyToExclude.getDependencyId()); dependency.addExclusion(exclusion); } - /** - * In order to keep the version as variable (property) for dependencies that were declared as such, post-process - * dependencies to replace interpolated version with the initial one. - */ - private void postProcessDependencies(List initialDependencies) { + @Override + protected void postProcessDependencies() { model.getDependencies().forEach(dep -> { for (Dependency initialDependency : initialDependencies) { if (hasVersionAsProperty(initialDependency) && matches(dep, initialDependency)) { @@ -105,23 +80,20 @@ private void postProcessDependencies(List initialDependencies) { }); } - private boolean hasVersionAsProperty(Dependency initialDependency) { - return initialDependency.getVersion().startsWith("$"); - } - - private void writeDebloatedPom() throws MojoExecutionException { + @Override + protected void writeFile() throws IOException { String pathToDebloatedPom = project.getBasedir().getAbsolutePath() + File.separator + "pom-debloated.xml"; - try { - Path path = Paths.get(pathToDebloatedPom); - writePom(path); - } catch (IOException e) { - throw new MojoExecutionException(e.getMessage(), e); - } + Path path = Paths.get(pathToDebloatedPom); + writePom(path); log.info("POM debloated successfully"); log.info("pom-debloated.xml file created in: " + pathToDebloatedPom); } + private boolean hasVersionAsProperty(Dependency initialDependency) { + return initialDependency.getVersion().startsWith("$"); + } + /** * Write pom file to the filesystem. * @@ -133,14 +105,14 @@ private void writePom(final Path pomFile) throws IOException { writer.write(Files.newBufferedWriter(pomFile), model); } - private Artifact findArtifact(se.kth.depclean.core.analysis.model.Dependency dependency) { + private Artifact findArtifact(se.kth.depclean.core.model.Dependency dependency) { return project.getArtifacts().stream() .filter(artifact -> matches(artifact, dependency)) .findFirst() .orElseThrow(() -> new RuntimeException("Unable to find " + dependency + " in dependencies")); } - private boolean matches(Artifact artifact, se.kth.depclean.core.analysis.model.Dependency coordinate) { + private boolean matches(Artifact artifact, se.kth.depclean.core.model.Dependency coordinate) { return coordinate.toString().toLowerCase().contains( String.format("%s:%s:%s", artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion()) .toLowerCase()); @@ -153,13 +125,13 @@ private boolean matches(Dependency dep, Dependency initialDependency) { /** * This method creates a Maven {@link Dependency} object from a depclean {@link - * se.kth.depclean.core.analysis.model.Dependency}. + * se.kth.depclean.core.model.Dependency}. * * @param dependency The depclean dependency to create the maven dependency. * @return The Dependency object. */ private Dependency createDependency( - final se.kth.depclean.core.analysis.model.Dependency dependency) { + final se.kth.depclean.core.model.Dependency dependency) { return createDependency(findArtifact(dependency)); } diff --git a/depclean-maven-plugin/src/main/java/se/kth/depclean/util/json/ParsedDependencies.java b/depclean-maven-plugin/src/main/java/se/kth/depclean/util/json/ParsedDependencies.java index 479f4807..93d78ad6 100644 --- a/depclean-maven-plugin/src/main/java/se/kth/depclean/util/json/ParsedDependencies.java +++ b/depclean-maven-plugin/src/main/java/se/kth/depclean/util/json/ParsedDependencies.java @@ -14,14 +14,12 @@ import java.io.Reader; import java.nio.charset.StandardCharsets; import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import se.kth.depclean.core.analysis.ProjectDependencyAnalysis; +import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis; /** * Uses the DepClean analysis results and the dependency tree of the project to produce a JSON file. This file represent * the structure of the dependency tree enriched with metadata of the usage or not of each dependency. */ -@Slf4j @AllArgsConstructor public class ParsedDependencies { diff --git a/depclean-maven-plugin/src/main/java/se/kth/depclean/wrapper/MavenDependencyManager.java b/depclean-maven-plugin/src/main/java/se/kth/depclean/wrapper/MavenDependencyManager.java new file mode 100644 index 00000000..b0726521 --- /dev/null +++ b/depclean-maven-plugin/src/main/java/se/kth/depclean/wrapper/MavenDependencyManager.java @@ -0,0 +1,217 @@ +package se.kth.depclean.wrapper; + +import com.google.common.collect.ImmutableSet; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Serializable; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import org.apache.commons.io.FileUtils; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Model; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.project.DefaultProjectBuildingRequest; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.ProjectBuildingRequest; +import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder; +import org.apache.maven.shared.dependency.graph.DependencyNode; +import org.codehaus.plexus.util.xml.Xpp3Dom; +import se.kth.depclean.core.AbstractDebloater; +import se.kth.depclean.core.analysis.graph.DependencyGraph; +import se.kth.depclean.core.analysis.model.ProjectDependencyAnalysis; +import se.kth.depclean.core.wrapper.DependencyManagerWrapper; +import se.kth.depclean.graph.MavenDependencyGraph; +import se.kth.depclean.util.JarUtils; +import se.kth.depclean.util.MavenDebloater; +import se.kth.depclean.util.MavenInvoker; +import se.kth.depclean.util.json.ParsedDependencies; + +/** + * Maven's implementation of the dependency manager wrapper. + */ +@AllArgsConstructor +public class MavenDependencyManager implements DependencyManagerWrapper { + + private static final String DIRECTORY_TO_COPY_DEPENDENCIES = "dependency"; + + private final Log logger; + private final MavenProject project; + private final MavenSession session; + private final DependencyGraphBuilder dependencyGraphBuilder; + private final Model model; + + /** + * Creates the manager. + * + * @param logger the logger + * @param project the maven project + * @param session the maven session + * @param dependencyGraphBuilder a tool to build the dependency graph + */ + public MavenDependencyManager(Log logger, MavenProject project, MavenSession session, + DependencyGraphBuilder dependencyGraphBuilder) { + this.logger = logger; + this.project = project; + this.session = session; + this.dependencyGraphBuilder = dependencyGraphBuilder; + + this.model = buildModel(project); + } + + @Override + public Log getLog() { + return logger; + } + + @Override + public boolean isMaven() { + return true; + } + + @Override + public boolean isPackagingPom() { + return project.getPackaging().equals("pom"); + } + + @Override + public void copyAndExtractDependencies() { + /* Copy direct dependencies locally */ + try { + MavenInvoker.runCommand("mvn dependency:copy-dependencies -DoutputDirectory=" + + project.getBuild().getDirectory() + File.separator + DIRECTORY_TO_COPY_DEPENDENCIES); + } catch (IOException | InterruptedException e) { + getLog().error("Unable to resolve all the dependencies."); + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + // TODO remove this workaround later + if (new File(project.getBuild().getDirectory() + File.separator + "libs").exists()) { + try { + FileUtils.copyDirectory(new File(project.getBuild().getDirectory() + File.separator + "libs"), + new File(project.getBuild().getDirectory() + File.separator + DIRECTORY_TO_COPY_DEPENDENCIES) + ); + } catch (IOException | NullPointerException e) { + getLog().error("Error copying directory libs to dependency"); + throw new RuntimeException(e); + } + } + + /* Decompress dependencies */ + String dependencyDirectoryName = + project.getBuild().getDirectory() + "/" + DIRECTORY_TO_COPY_DEPENDENCIES; + File dependencyDirectory = new File(dependencyDirectoryName); + if (dependencyDirectory.exists()) { + JarUtils.decompress(dependencyDirectoryName); + } + } + + @Override + @SneakyThrows + public DependencyGraph dependencyGraph() { + ProjectBuildingRequest buildingRequest = new DefaultProjectBuildingRequest( + session.getProjectBuildingRequest()); + buildingRequest.setProject(project); + DependencyNode rootNode = dependencyGraphBuilder.buildDependencyGraph(buildingRequest, null); + return new MavenDependencyGraph(project, model, rootNode); + } + + @Override + public Path getOutputDirectory() { + return Paths.get(project.getBuild().getOutputDirectory()); + } + + @Override + public Path getTestOutputDirectory() { + return Paths.get(project.getBuild().getTestOutputDirectory()); + } + + private Model buildModel(MavenProject project) { + File pomFile = new File(project.getBasedir().getAbsolutePath() + File.separator + "pom.xml"); + + /* Build Maven model to manipulate the pom */ + final Model model; + FileReader reader; + MavenXpp3Reader mavenReader = new MavenXpp3Reader(); + try { + reader = new FileReader(pomFile); + model = mavenReader.read(reader); + model.setPomFile(pomFile); + } catch (Exception ex) { + getLog().error("Unable to build the maven project."); + throw new RuntimeException(ex); + } + return model; + } + + /** + * Maven processors are defined like this. + *
{@code
+   *       
+   *         org.bsc.maven
+   *         maven-processor-plugin
+   *         
+   *           
+   *             process
+   *             [...]
+   *             
+   *               
+   *                 XXXProcessor
+   *               
+   *             
+   *           
+   *         
+   *       
+   * }
+ */ + @Override + public Set collectUsedClassesFromProcessors() { + getLog().debug("# collectUsedClassesFromProcessors()"); + return Optional.ofNullable(project.getPlugin("org.bsc.maven:maven-processor-plugin")) + .map(plugin -> plugin.getExecutionsAsMap().get("process")) + .map(exec -> (Xpp3Dom) exec.getConfiguration()) + .map(config -> config.getChild("processors")) + .map(Xpp3Dom::getChildren) + .map(arr -> Arrays.stream(arr).map(Xpp3Dom::getValue).collect(Collectors.toSet())) + .orElse(ImmutableSet.of()); + } + + @Override + public AbstractDebloater getDebloater(ProjectDependencyAnalysis analysis) { + return new MavenDebloater( + analysis, + project, + model + ); + } + + @Override + public String getBuildDirectory() { + return project.getBuild().getDirectory(); + } + + @Override + public void generateDependencyTree(File treeFile) throws IOException, InterruptedException { + MavenInvoker.runCommand("mvn dependency:tree -DoutputFile=" + treeFile + " -Dverbose=true"); + } + + @SneakyThrows + @Override + public String getTreeAsJson( + File treeFile, ProjectDependencyAnalysis analysis, File classUsageFile, boolean createClassUsageCsv) { + return new ParsedDependencies( + treeFile, + analysis, + classUsageFile, + createClassUsageCsv + ).parseTreeToJson(); + } +}