/* * Copyright 2016 Lukas Krejci * * 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.revapi.maven; import static org.apache.maven.plugins.annotations.LifecyclePhase.PACKAGE; import static org.apache.maven.plugins.annotations.LifecyclePhase.SITE; import java.io.File; import java.text.MessageFormat; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.ResourceBundle; import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.maven.doxia.sink.Sink; import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Plugin; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Execute; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.project.MavenProject; import org.apache.maven.reporting.MavenReportException; import org.codehaus.plexus.util.xml.Xpp3Dom; import org.eclipse.aether.DefaultRepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.repository.RepositoryPolicy; import org.eclipse.aether.resolution.ArtifactResolutionException; import org.eclipse.aether.resolution.VersionRangeResolutionException; import org.revapi.API; import org.revapi.Reporter; import org.revapi.Revapi; import org.revapi.maven.utils.ArtifactResolver; import org.revapi.maven.utils.ScopeDependencySelector; import org.revapi.maven.utils.ScopeDependencyTraverser; /** * Uses the configuration supplied at the top level aggregator project to run analysis on all sub-projects. * <p> * The artifacts to compare are taken from the configurations of the child projects while the configuration of Revapi * and the extensions to use are taken from the aggregator project. The analyses are run in succession using a single * instance of Revapi. Therefore you need to configure your custom Revapi reporter(s) to somehow not overwrite their * reports, but append to it. The default site page generator can do this and the {@code revapi-reporter-text} reporter * has an {@code append} boolean parameter for this. If you're using some other reporter, consult its documentation on * how to append to a report instead of overwriting it. * * @author Lukas Krejci * @since 0.5.0 */ @Mojo(name = "report-aggregate", aggregator = true, defaultPhase = SITE) @Execute(phase = PACKAGE) public class ReportAggregateMojo extends ReportMojo { @Component private MavenSession mavenSession; @Override public String getOutputName() { return "revapi-aggregate-report"; } @Override public File getReportOutputDirectory() { return new File(mavenSession.getTopLevelProject().getBasedir(), "target/site"); } @Override protected String getOutputDirectory() { return getReportOutputDirectory().getAbsolutePath(); } @Override public void setReportOutputDirectory(File reportOutputDirectory) { //this is called by the site plugin to set the output directory. We grandiously ignore what it wants and output //in the top level project's site dir. super.setReportOutputDirectory(getReportOutputDirectory()); } @Override public String getDescription(Locale locale) { return null; } @Override public boolean canGenerateReport() { //aggregate report makes sense only for POM return "pom".equals(project.getArtifact().getArtifactHandler().getPackaging()); } @Override protected void executeReport(Locale locale) throws MavenReportException { if (skip) { return; } if (!canGenerateReport()) { return; } List<MavenProject> dependents = mavenSession.getProjectDependencyGraph().getDownstreamProjects(project, true); Collections.sort(dependents, (a, b) -> { String as = a.getArtifact().toString(); String bs = b.getArtifact().toString(); return as.compareTo(bs); }); Map<MavenProject, ProjectVersions> projectVersions = dependents.stream().collect( Collectors.toMap(Function.identity(), this::getRunConfig)); projectVersions.put(project, getRunConfig(project)); ResourceBundle messages = getBundle(locale); Sink sink = getSink(); ReportTimeReporter reporter = null; if (generateSiteReport) { startReport(sink, messages); reporter = new ReportTimeReporter(reportSeverity.asDifferenceSeverity()); } try (Analyzer topAnalyzer = prepareAnalyzer(null, project, locale, reporter, projectVersions.get(project))) { Revapi sharedRevapi = topAnalyzer == null ? null : topAnalyzer.getRevapi(); for (MavenProject p : dependents) { try (Analyzer projectAnalyzer = prepareAnalyzer(sharedRevapi, p, locale, reporter, projectVersions.get(p))) { if (projectAnalyzer != null) { projectAnalyzer.analyze(); if (generateSiteReport) { reportBody(reporter, projectAnalyzer.getResolvedOldApi(), projectAnalyzer.getResolvedNewApi(), sink, messages); } } } } if (generateSiteReport) { endReport(sink); } } catch (Exception e) { throw new MavenReportException("Failed to generate the report.", e); } } @Override protected void reportBody(ReportTimeReporter reporterWithResults, API oldAPI, API newAPI, Sink sink, ResourceBundle messages) { if (oldAPI == null || newAPI == null) { return; } sink.section2(); sink.sectionTitle2(); String title = messages.getString("report.revapi.aggregate.subTitle"); sink.rawText(MessageFormat.format(title, niceList(oldAPI.getArchives()), niceList(newAPI.getArchives()))); sink.sectionTitle2_(); super.reportBody(reporterWithResults, oldAPI, newAPI, sink, messages); sink.section2_(); } private ProjectVersions getRunConfig(MavenProject project) { ProjectVersions ret = new ProjectVersions(); Plugin revapiPlugin = findRevapi(project); if (revapiPlugin == null) { return ret; } Xpp3Dom pluginConfig = (Xpp3Dom) revapiPlugin.getConfiguration(); String[] oldArtifacts = getArtifacts(pluginConfig, "oldArtifacts"); String[] newArtifacts = getArtifacts(pluginConfig, "newArtifacts"); String oldVersion = getValueOfChild(pluginConfig, "oldVersion"); if (oldVersion == null) { oldVersion = System.getProperties().getProperty(Props.oldVersion.NAME, Props.oldVersion.DEFAULT_VALUE); } String newVersion = getValueOfChild(pluginConfig, "newVersion"); if (newVersion == null) { newVersion = System.getProperties().getProperty(Props.newVersion.NAME, project.getVersion()); } String defaultOldArtifact = Analyzer.getProjectArtifactCoordinates(project, oldVersion); String defaultNewArtifact = Analyzer.getProjectArtifactCoordinates(project, newVersion); if (oldArtifacts == null || oldArtifacts.length == 0) { if (!project.getArtifact().getArtifactHandler().isAddedToClasspath()) { return ret; } oldArtifacts = new String[]{defaultOldArtifact}; } if (newArtifacts == null || newArtifacts.length == 0) { if (!project.getArtifact().getArtifactHandler().isAddedToClasspath()) { return ret; } newArtifacts = new String[]{defaultNewArtifact}; } String versionRegexString = getValueOfChild(pluginConfig, "versionFormat"); Pattern versionRegex = versionRegexString == null ? null : Pattern.compile(versionRegexString); DefaultRepositorySystemSession session = new DefaultRepositorySystemSession(repositorySystemSession); session.setDependencySelector(new ScopeDependencySelector("compile", "provided")); session.setDependencyTraverser(new ScopeDependencyTraverser("compile", "provided")); if (alwaysCheckForReleaseVersion) { session.setUpdatePolicy(RepositoryPolicy.UPDATE_POLICY_ALWAYS); } ArtifactResolver resolver = new ArtifactResolver(repositorySystem, session, mavenSession.getCurrentProject().getRemoteProjectRepositories()); Function<String, Artifact> resolve = gav -> { try { return Analyzer.resolveConstrained(project, gav, versionRegex, resolver); } catch (VersionRangeResolutionException | ArtifactResolutionException e) { getLog().warn("Could not resolve artifact '" + gav + "' with message: " + e.getMessage()); return null; } }; ret.oldGavs = Stream.of(oldArtifacts).map(resolve).filter(f -> f != null).toArray(Artifact[]::new); ret.newGavs = Stream.of(newArtifacts).map(resolve).filter(f -> f != null).toArray(Artifact[]::new); return ret; } private Analyzer prepareAnalyzer(Revapi revapi, MavenProject project, Locale locale, Reporter defaultReporter, ProjectVersions storedVersions) { Plugin runPluginConfig = findRevapi(project); if (runPluginConfig == null) { return null; } Xpp3Dom runConfig = (Xpp3Dom) runPluginConfig.getConfiguration(); Artifact[] oldArtifacts = storedVersions.oldGavs; Artifact[] newArtifacts = storedVersions.newGavs; if (oldArtifacts == null || oldArtifacts.length == 0 || newArtifacts == null || newArtifacts.length == 0) { return null; } boolean failOnMissingConfigurationFiles = false; boolean failOnMissingArchives = false; boolean failOnMissingSupportArchives = false; boolean alwaysUpdate = true; boolean resolveDependencies = true; String versionRegex = getValueOfChild(runConfig, "versionFormat"); if (revapi == null) { final List<String> disallowedExtensions = this.disallowedExtensions == null ? Collections.emptyList() : Arrays.asList(this.disallowedExtensions.split("\\s*,\\s*")); Supplier<Revapi.Builder> ctor = AbstractRevapiMojo.getDisallowedExtensionsAwareRevapiConstructor(disallowedExtensions); return new Analyzer(this.analysisConfiguration, this.analysisConfigurationFiles, oldArtifacts, newArtifacts, project, repositorySystem, repositorySystemSession, defaultReporter, locale, getLog(), failOnMissingConfigurationFiles, failOnMissingArchives, failOnMissingSupportArchives, alwaysUpdate, resolveDependencies, versionRegex, ctor); } else { return new Analyzer(this.analysisConfiguration, this.analysisConfigurationFiles, oldArtifacts, newArtifacts, project, repositorySystem, repositorySystemSession, null, locale, getLog(), failOnMissingConfigurationFiles, failOnMissingArchives, failOnMissingSupportArchives, alwaysUpdate, resolveDependencies, versionRegex, revapi); } } protected static Plugin findRevapi(MavenProject project) { return project.getBuildPlugins().stream() .filter(p -> "org.revapi:revapi-maven-plugin".equals(p.getKey())) .findAny().orElse(null); } protected static String[] getArtifacts(Xpp3Dom config, String artifactTag) { Xpp3Dom oldArtifactsXml = config == null ? null : config.getChild(artifactTag); if (oldArtifactsXml == null) { return new String[0]; } if (oldArtifactsXml.getChildCount() == 0) { String artifact = oldArtifactsXml.getValue(); return new String[]{artifact}; } else { String[] ret = new String[oldArtifactsXml.getChildCount()]; for (int i = 0; i < oldArtifactsXml.getChildCount(); ++i) { ret[i] = oldArtifactsXml.getChild(i).getValue(); } return ret; } } private static String getValueOfChild(Xpp3Dom element, String childName) { Xpp3Dom child = element == null ? null : element.getChild(childName); return child == null ? null : child.getValue(); } private static final class ProjectVersions { Artifact[] oldGavs; Artifact[] newGavs; } }