/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.ServiceLoader;
import java.util.function.Supplier;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.revapi.ApiAnalyzer;
import org.revapi.DifferenceTransform;
import org.revapi.ElementFilter;
import org.revapi.Reporter;
import org.revapi.Revapi;
/**
* @author Lukas Krejci
* @since 0.3.11
*/
abstract class AbstractRevapiMojo extends AbstractMojo {
/**
* The JSON configuration of various analysis options. The available options depend on what
* analyzers are present on the plugins classpath through the {@code <dependencies>}.
*
* <p>These settings take precedence over the configuration loaded from {@code analysisConfigurationFiles}.
*/
@Parameter(property = Props.analysisConfiguration.NAME, defaultValue = Props.analysisConfiguration.DEFAULT_VALUE)
protected String analysisConfiguration;
/**
* Set to false if you want to tolerate files referenced in the {@code analysisConfigurationFiles} missing on the
* filesystem and therefore not contributing to the analysis configuration.
*
* <p>The default is {@code true}, which means that a missing analysis configuration file will fail the build.
*/
@Parameter(property = Props.failOnMissingConfigurationFiles.NAME, defaultValue = Props.failOnMissingConfigurationFiles.DEFAULT_VALUE)
protected boolean failOnMissingConfigurationFiles;
/**
* The list of files containing the configuration of various analysis options.
* The available options depend on what analyzers are present on the plugins classpath through the
* {@code <dependencies>}.
*
* <p>The {@code analysisConfiguration} can override the settings present in the files.
*
* <p>The list is either a list of strings or has the following form:
* <pre><code>
* <analysisConfigurationFiles>
* <configurationFile>
* <path>path/to/the/file/relative/to/project/base/dir</path>
* <roots>
* <root>configuration/root1</root>
* <root>configuration/root2</root>
* ...
* </roots>
* </configurationFile>
* ...
* </analysisConfigurationFiles>
* </code></pre>
*
* where
* <ul>
* <li>{@code path} is mandatory,</li>
* <li>{@code roots} is optional and specifies the subtrees of the JSON config that should be used for
* configuration. If not specified, the whole file is taken into account.</li>
* </ul>
* The {@code configuration/root1} and {@code configuration/root2} are JSON paths to the roots of the
* configuration inside that JSON config file. This might be used in cases where multiple configurations are stored
* within a single file and you want to use a particular one.
*
* <p>An example of this might be a config file which contains API changes to be ignored in all past versions of a
* library. The classes to be ignored are specified in a configuration that is specific for each version:
* <pre><code>
* {
* "0.1.0" : {
* "revapi" : {
* "ignore" : [
* {
* "code" : "java.method.addedToInterface",
* "new" : "method void com.example.MyInterface::newMethod()",
* "justification" : "This interface is not supposed to be implemented by clients."
* },
* ...
* ]
* }
* },
* "0.2.0" : {
* ...
* }
* }
* </code></pre>
*/
@Parameter(property = Props.analysisConfigurationFiles.NAME, defaultValue = Props.analysisConfigurationFiles.DEFAULT_VALUE)
protected Object[] analysisConfigurationFiles;
/**
* The coordinates of the old artifacts. Defaults to single artifact with the latest released version of the
* current project.
* <p/>
* If the this property is null, the {@link #oldVersion} property is checked for a value of the old version of the
* artifact being built.
*
* @see #oldVersion
*/
@Parameter(property = Props.oldArtifacts.NAME, defaultValue = Props.oldArtifacts.DEFAULT_VALUE)
protected String[] oldArtifacts;
/**
* If you don't want to compare a different artifact than the one being built, specifying the just the old version
* is simpler way of specifying the old artifact.
* <p/>
* The default value is "RELEASE" meaning that the old version is the last released version of the artifact being
* built.
*/
@Parameter(property = Props.oldVersion.NAME, defaultValue = Props.oldVersion.DEFAULT_VALUE)
protected String oldVersion;
/**
* The coordinates of the new artifacts. These are the full GAVs of the artifacts, which means that you can compare
* different artifacts than the one being built. If you merely want to specify the artifact being built, use
* {@link #newVersion} property instead.
*/
@Parameter(property = Props.newArtifacts.NAME, defaultValue = Props.newArtifacts.DEFAULT_VALUE)
protected String[] newArtifacts;
/**
* The new version of the artifact. Defaults to "${project.version}".
*/
@Parameter(property = Props.newVersion.NAME, defaultValue = Props.newVersion.DEFAULT_VALUE)
protected String newVersion;
/**
* Whether to skip the mojo execution.
*/
@Parameter(property = Props.skip.NAME, defaultValue = Props.skip.DEFAULT_VALUE)
protected boolean skip;
/**
* The severity of found problems at which to break the build. Defaults to API breaking changes.
* Possible values: equivalent, nonBreaking, potentiallyBreaking, breaking.
*/
@Parameter(property = Props.failSeverity.NAME, defaultValue = Props.failSeverity.DEFAULT_VALUE)
protected FailSeverity failSeverity;
@Parameter(defaultValue = "${project}", readonly = true)
protected MavenProject project;
@Component
protected RepositorySystem repositorySystem;
@Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
protected RepositorySystemSession repositorySystemSession;
/**
* If true (the default) revapi will always download the information about the latest version from the remote
* repositories (instead of using locally cached info). This will respect the offline settings.
*/
@Parameter(property = Props.alwaysCheckForReleaseVersion.NAME, defaultValue = Props.alwaysCheckForReleaseVersion.DEFAULT_VALUE)
protected boolean alwaysCheckForReleaseVersion;
/**
* If true (the default), the maven plugin will fail the build when it finds API problems.
*/
@Parameter(property = Props.failBuildOnProblemsFound.NAME, defaultValue = Props.failBuildOnProblemsFound.DEFAULT_VALUE)
protected boolean failBuildOnProblemsFound;
/**
* If true, the build will fail if one of the old or new artifacts fails to be resolved. Defaults to false.
*/
@Parameter(property = Props.failOnUnresolvedArtifacts.NAME, defaultValue = Props.failOnUnresolvedArtifacts.DEFAULT_VALUE)
protected boolean failOnUnresolvedArtifacts;
/**
* If true, the build will fail if some of the dependencies of the old or new artifacts fail to be resolved.
* Defaults to false.
*/
@Parameter(property = Props.failOnUnresolvedDependencies.NAME, defaultValue = Props.failOnUnresolvedDependencies.DEFAULT_VALUE)
protected boolean failOnUnresolvedDependencies;
/**
* Whether to include the dependencies in the API checks. This is the default thing to do because your API might
* be exposing classes from the dependencies and thus classes from your dependencies could become part of your API.
* <p>
* However, setting this to false might be useful in situations where you have checked your dependencies in another
* module and don't want do that again. In that case, you might want to configure Revapi to ignore missing classes
* because it might find the classes from your dependencies as used in your API and would complain that it could not
* find it. See <a href="http://revapi.org/modules/revapi-java/extensions/java.html">the docs</a>.
*/
@Parameter(property = Props.checkDependencies.NAME, defaultValue = Props.checkDependencies.DEFAULT_VALUE)
protected boolean checkDependencies;
/**
* If set, this property demands a format of the version string when the {@link #oldVersion} or {@link #newVersion}
* parameters are set to {@code RELEASE} or {@code LATEST} special version strings.
* <p>
* Because Maven will report the newest non-snapshot version as the latest release, we might end up comparing a
* {@code .Beta} or other pre-release versions with the new version. This might not be what you want and setting the
* versionFormat will make sure that a newest version conforming to the version format is used instead of the one
* resolved by Maven by default.
* <p>
* This parameter is a regular expression pattern that the version string needs to match in order to be considered
* a {@code RELEASE}.
*/
@Parameter(property = Props.versionFormat.NAME, defaultValue = Props.versionFormat.DEFAULT_VALUE)
protected String versionFormat;
/**
* A comma-separated list of extensions (fully-qualified class names thereof) that are not taken into account during
* API analysis. By default, all extensions that are found on the classpath are used.
* <p>
* You can modify this set if you use another extensions that change the found differences in a way that the
* determined new version would not correspond to what it should be.
*/
@Parameter(property = Props.disallowedExtensions.NAME, defaultValue = Props.disallowedExtensions.DEFAULT_VALUE)
protected String disallowedExtensions;
protected void analyze(Reporter reporter) throws MojoExecutionException, MojoFailureException {
try (Analyzer analyzer = prepareAnalyzer(reporter)) {
if (analyzer != null) {
analyzer.analyze();
}
} catch (MojoExecutionException e) {
throw e;
} catch (Exception e) {
throw new MojoExecutionException("Failed to close the API analyzer.", e);
}
}
protected Analyzer prepareAnalyzer(Reporter reporter) {
if (skip) {
return null;
}
if (!initializeComparisonArtifacts()) {
return null;
}
final List<String> disallowedExtensions = this.disallowedExtensions == null
? Collections.emptyList()
: Arrays.asList(this.disallowedExtensions.split("\\s*,\\s*"));
Supplier<Revapi.Builder> ctor = getDisallowedExtensionsAwareRevapiConstructor(disallowedExtensions);
return new Analyzer(analysisConfiguration, analysisConfigurationFiles, oldArtifacts,
newArtifacts, project, repositorySystem, repositorySystemSession, reporter, Locale.getDefault(), getLog(),
failOnMissingConfigurationFiles, failOnUnresolvedArtifacts, failOnUnresolvedDependencies,
alwaysCheckForReleaseVersion, checkDependencies, versionFormat, ctor);
}
/**
* @return true if artifacts are initialized, false if not and the analysis should not proceed
*/
protected boolean initializeComparisonArtifacts() {
if (newArtifacts != null && newArtifacts.length == 1 && "BUILD".equals(newArtifacts[0])) {
getLog().warn("\"BUILD\" coordinates are deprecated. Just leave \"newArtifacts\" undefined and specify" +
" \"${project.version}\" as the value for \"newVersion\" (which is the default, so you don't" +
" actually have to do that either).");
oldArtifacts = null;
}
if (oldArtifacts == null || oldArtifacts.length == 0) {
//non-intuitively, we need to initialize the artifacts even if we will not proceed with the analysis itself
//that's because we need know the versions when figuring out the version modifications -
//see AbstractVersionModifyingMojo
oldArtifacts = new String[]{
Analyzer.getProjectArtifactCoordinates(project, oldVersion)};
//bail out quickly for POM artifacts (or any other packaging without a file result) - there's nothing we can
//analyze there
//only do it here, because oldArtifacts might point to another artifact.
//if we end up here in this branch, we know we'll be comparing the current artifact with something.
if (!project.getArtifact().getArtifactHandler().isAddedToClasspath()) {
return false;
}
}
if (newArtifacts == null || newArtifacts.length == 0) {
newArtifacts = new String[]{
Analyzer.getProjectArtifactCoordinates(project, newVersion)};
//bail out quickly for POM artifacts (or any other packaging without a file result) - there's nothing we can
//analyze there
//again, do this check only here, because oldArtifact might point elsewhere. But if we end up here, it
//means that oldArtifacts would be compared against the current artifact (in some version). Comparing
//against a POM artifact is always no-op.
if (!project.getArtifact().getArtifactHandler().isAddedToClasspath()) {
return false;
}
}
return true;
}
protected static Supplier<Revapi.Builder>
getDisallowedExtensionsAwareRevapiConstructor(List<String> disallowedExtensions) {
return () -> {
Revapi.Builder bld = Revapi.builder();
List<ApiAnalyzer> analyzers = new ArrayList<>();
List<ElementFilter> filters = new ArrayList<>();
List<DifferenceTransform<?>> transforms = new ArrayList<>();
List<Reporter> reporters = new ArrayList<>();
addAllAllowed(analyzers, ServiceLoader.load(ApiAnalyzer.class), disallowedExtensions);
addAllAllowed(filters, ServiceLoader.load(ElementFilter.class), disallowedExtensions);
addAllAllowed(transforms, ServiceLoader.load(DifferenceTransform.class), disallowedExtensions);
addAllAllowed(reporters, ServiceLoader.load(Reporter.class), disallowedExtensions);
bld.withAnalyzers(analyzers).withFilters(filters).withTransforms(transforms).withReporters(reporters);
return bld;
};
}
@SuppressWarnings("unchecked")
protected static <T> void addAllAllowed(List<T> list, Iterable<?> candidates, List<String> disallowedClassNames) {
for (Object o : candidates) {
if (o != null && !disallowedClassNames.contains(o.getClass().getName())) {
list.add((T) o);
}
}
}
}