org.revapi.maven.AbstractVersionModifyingMojo Maven / Gradle / Ivy
Show all versions of revapi-maven-plugin Show documentation
/*
* Copyright $year 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 java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.apache.maven.execution.MavenSession;
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.artifact.DefaultArtifact;
import com.ximpleware.AutoPilot;
import com.ximpleware.ModifyException;
import com.ximpleware.NavException;
import com.ximpleware.TranscodeException;
import com.ximpleware.VTDGen;
import com.ximpleware.VTDNav;
import com.ximpleware.XMLModifier;
import com.ximpleware.XPathParseException;
/**
* @author Lukas Krejci
* @since 0.4.0
*/
class AbstractVersionModifyingMojo extends AbstractRevapiMojo {
@Component
protected MavenSession mavenSession;
/**
* Set to true if all the projects in the current build should share a single version (the default is false).
* This is useful if your distribution consists of several modules that make up one logical unit that is always
* versioned the same.
*
* In this case all the projects in the current build are API-checked and the version is determined by the
* "biggest" API change found in all the projects. I.e. if just a single module breaks API then all of the
* modules will get the major version incremented.
*/
@Parameter(property = Props.singleVersionForAllModules.NAME, defaultValue = Props.singleVersionForAllModules.DEFAULT_VALUE)
private boolean singleVersionForAllModules;
/**
* A comma-separated list of extensions (fully-qualified class names thereof) that are not taken into account during
* API analysis for versioning purposes. By default, only the semver-ignore transform is not taken into account so
* that it does not interfere with the purpose of modifying the version based on the semver rules.
*
* 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.disallowedExtensionsInVersioning.NAME, defaultValue = Props.disallowedExtensionsInVersioning.DEFAULT_VALUE)
protected String disallowedExtensions;
private boolean preserveSuffix;
private String replacementSuffix;
public boolean isPreserveSuffix() {
return preserveSuffix;
}
void setPreserveSuffix(boolean preserveSuffix) {
this.preserveSuffix = preserveSuffix;
}
public String getReplacementSuffix() {
return replacementSuffix;
}
void setReplacementSuffix(String replacementSuffix) {
this.replacementSuffix = replacementSuffix;
}
public boolean isSingleVersionForAllModules() {
return singleVersionForAllModules;
}
@Override public void execute() throws MojoExecutionException, MojoFailureException {
if (skip) {
return;
}
AnalysisResults analysisResults;
if (!initializeComparisonArtifacts()) {
//we've got non-file artifacts, for which there is no reason to run analysis
DefaultArtifact oldArtifact = new DefaultArtifact(oldArtifacts[0]);
analysisResults = new AnalysisResults(ApiChangeLevel.NO_CHANGE, oldArtifact.getVersion());
} else {
analysisResults = analyzeProject(project);
}
if (analysisResults == null) {
return;
}
ApiChangeLevel changeLevel = analysisResults.apiChangeLevel;
if (singleVersionForAllModules) {
File changesFile = getChangesFile();
try (PrintWriter out = new PrintWriter(new FileOutputStream(changesFile, true))) {
out.println(project.getArtifact().toString() + "=" + changeLevel + "," + analysisResults.baseVersion);
} catch (IOException e) {
throw new MojoExecutionException("Failure while updating the changes tracking file.", e);
}
} else {
Version v = nextVersion(analysisResults.baseVersion, changeLevel);
updateProjectVersion(project, v);
}
if (singleVersionForAllModules
&& project.equals(mavenSession.getProjects().get(mavenSession.getProjects().size() - 1))) {
try (BufferedReader rdr = new BufferedReader(new FileReader(getChangesFile()))) {
Map projectChanges = new HashMap<>();
String line;
while ((line = rdr.readLine()) != null) {
int equalsIdx = line.indexOf('=');
String projectGav = line.substring(0, equalsIdx);
String changeAndBaseVersion = line.substring(equalsIdx + 1);
int commaIdx = changeAndBaseVersion.indexOf(',');
String change = changeAndBaseVersion.substring(0, commaIdx);
String baseVersion = changeAndBaseVersion.substring(commaIdx + 1);
changeLevel = ApiChangeLevel.valueOf(change);
projectChanges.put(projectGav, new AnalysisResults(changeLevel, baseVersion));
}
//establish the tree hierarchy of the projects
Set roots = new HashSet<>();
Map> children = new HashMap<>();
Deque unprocessed = new ArrayDeque<>(mavenSession.getProjects());
while (!unprocessed.isEmpty()) {
MavenProject pr = unprocessed.pop();
if (!projectChanges.containsKey(pr.getArtifact().toString())) {
continue;
}
MavenProject pa = pr.getParent();
if (roots.contains(pa)) {
roots.remove(pr);
AnalysisResults paR = projectChanges.get(pa.getArtifact().toString());
AnalysisResults prR = projectChanges.get(pr.getArtifact().toString());
if (prR.apiChangeLevel.ordinal() > paR.apiChangeLevel.ordinal()) {
paR.apiChangeLevel = prR.apiChangeLevel;
}
children.get(pa).add(pr);
} else {
roots.add(pr);
}
children.put(pr, new HashSet());
}
Iterator it = roots.iterator();
while (it.hasNext()) {
Deque tree = new ArrayDeque<>();
MavenProject p = it.next();
tree.add(p);
it.remove();
AnalysisResults results = projectChanges.get(p.getArtifact().toString());
Version v = nextVersion(results.baseVersion, results.apiChangeLevel);
while (!tree.isEmpty()) {
MavenProject current = tree.pop();
updateProjectVersion(current, v);
Set c = children.get(current);
if (c != null) {
for (MavenProject cp : c) {
updateProjectParentVersion(cp, v);
}
tree.addAll(c);
}
}
}
} catch (IOException e) {
throw new MojoExecutionException("Failure while reading the changes tracking file.", e);
}
}
}
private File getChangesFile() throws MojoExecutionException {
File targetDir = new File(mavenSession.getExecutionRootDirectory(), "target");
if (!targetDir.exists() && !targetDir.mkdirs()) {
throw new MojoExecutionException("Failed to create the target directory: " + targetDir);
}
File updateVersionsDir = new File(targetDir, "revapi-update-versions");
if (!updateVersionsDir.exists() && !updateVersionsDir.mkdirs()) {
throw new MojoExecutionException("Failed to create the revapi-update-versions directory: " + targetDir);
}
return new File(updateVersionsDir, "per-project.changes");
}
void updateProjectVersion(MavenProject project, Version version) throws MojoExecutionException {
try {
VTDGen gen = new VTDGen();
gen.enableIgnoredWhiteSpace(true);
gen.parseFile(project.getFile().getAbsolutePath(), true);
VTDNav nav = gen.getNav();
AutoPilot ap = new AutoPilot(nav);
ap.selectXPath("namespace-uri(.)");
String ns = ap.evalXPathToString();
nav.toElementNS(VTDNav.FIRST_CHILD, ns, "version");
int pos = nav.getText();
XMLModifier mod = new XMLModifier(nav);
mod.updateToken(pos, version.toString());
try (OutputStream out = new FileOutputStream(project.getFile())) {
mod.output(out);
}
} catch (IOException | ModifyException | NavException | XPathParseException | TranscodeException e) {
throw new MojoExecutionException("Failed to update the version of project " + project, e);
}
}
void updateProjectParentVersion(MavenProject project, Version version) throws MojoExecutionException {
try {
VTDGen gen = new VTDGen();
gen.enableIgnoredWhiteSpace(true);
gen.parseFile(project.getFile().getAbsolutePath(), true);
VTDNav nav = gen.getNav();
AutoPilot ap = new AutoPilot(nav);
ap.selectXPath("namespace-uri(.)");
String ns = ap.evalXPathToString();
nav.toElementNS(VTDNav.FIRST_CHILD, ns, "parent");
nav.toElementNS(VTDNav.FIRST_CHILD, ns, "version");
int pos = nav.getText();
XMLModifier mod = new XMLModifier(nav);
mod.updateToken(pos, version.toString());
try (OutputStream out = new FileOutputStream(project.getFile())) {
mod.output(out);
}
} catch (IOException | ModifyException | NavException | XPathParseException | TranscodeException e) {
throw new MojoExecutionException("Failed to update the parent version of project " + project, e);
}
}
private Version nextVersion(String baseVersion, ApiChangeLevel changeLevel) {
Version v = Version.parse(project.getVersion());
boolean isDev = v.getMajor() == 0;
switch (changeLevel) {
case NO_CHANGE:
break;
case NON_BREAKING_CHANGES:
if (isDev) {
v.setPatch(v.getPatch() + 1);
} else {
v.setMinor(v.getMinor() + 1);
v.setPatch(0);
}
break;
case BREAKING_CHANGES:
if (isDev) {
v.setMinor(v.getMinor() + 1);
v.setPatch(0);
} else {
v.setMajor(v.getMajor() + 1);
v.setMinor(0);
v.setPatch(0);
}
break;
default:
throw new IllegalArgumentException("Unhandled API change level: " + changeLevel);
}
if (replacementSuffix != null) {
String sep = replacementSuffix.substring(0, 1);
String suffix = replacementSuffix.substring(1);
v.setSuffixSeparator(sep);
v.setSuffix(suffix);
} else if (!preserveSuffix) {
v.setSuffix(null);
v.setSuffixSeparator(null);
}
return v;
}
private AnalysisResults analyzeProject(MavenProject project) throws MojoExecutionException {
ApiBreakageHintingReporter reporter = new ApiBreakageHintingReporter();
try (Analyzer analyzer = prepareAnalyzer(project, reporter)) {
analyzer.resolveArtifacts();
if (analyzer.getResolvedOldApi() == null) {
return null;
} else {
analyzer.analyze();
ApiChangeLevel level = reporter.getChangeLevel();
String baseVersion = ((MavenArchive) analyzer.getResolvedOldApi().getArchives().iterator().next())
.getVersion();
return new AnalysisResults(level, baseVersion);
}
} catch (Exception e) {
throw new MojoExecutionException("Analysis failure", e);
}
}
private static final class AnalysisResults {
ApiChangeLevel apiChangeLevel;
final String baseVersion;
AnalysisResults(ApiChangeLevel apiChangeLevel, String baseVersion) {
this.apiChangeLevel = apiChangeLevel;
this.baseVersion = baseVersion;
}
}
}