blob: 1d296df4daf443cf49b213930c862c6291b95005 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.maven.dist.tools.source;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.apache.maven.dist.tools.AbstractDistCheckReport;
import org.apache.maven.dist.tools.ConfigurationLineInfo;
import org.apache.maven.dist.tools.JsoupRetry;
import org.apache.maven.doxia.markup.HtmlMarkup;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.reporting.MavenReportException;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
/**
* Check presence of source-release.zip in distribution area and central repo
*
* @author skygo
*/
@Mojo(name = "check-source-release", requiresProject = false)
public class DistCheckSourceReleaseReport extends AbstractDistCheckReport {
private static final String NOT_IN_DISTRIBUTION_AREA = "_not_in_distribution_area_";
/** Constant <code>FAILURES_FILENAME="check-source-release.log"</code> */
public static final String FAILURES_FILENAME = "check-source-release.log";
/**
* Dist Check Source Release Report
*/
public DistCheckSourceReleaseReport() {}
/** {@inheritDoc} */
@Override
protected boolean isIndexPageCheck() {
return false;
}
/**
* Ignore dist failure for <code>artifactId</code> or <code>artifactId:version</code>
*/
@Parameter
protected List<String> ignoreDistFailures;
/**
* <p>getFailuresFilename.</p>
*
* @return a {@link java.lang.String} object
*/
protected String getFailuresFilename() {
return FAILURES_FILENAME;
}
/** {@inheritDoc} */
@Override
public String getName(Locale locale) {
return "Dist Tool> Check Source Release";
}
/** {@inheritDoc} */
@Override
public String getDescription(Locale locale) {
return "Verification of source release";
}
private final List<CheckSourceReleaseResult> results = new LinkedList<>();
private static class DirectoryStatistics {
final String directory;
final String groupId;
int artifactsCount = 0;
int centralMissing = 0;
int distError = 0;
int distMissing = 0;
int distOlder = 0;
private DirectoryStatistics(String directory, String groupId) {
this.directory = directory;
this.groupId = groupId;
}
public boolean contains(CheckSourceReleaseResult csrr) {
return csrr.getConfigurationLine().getDirectory().equals(directory);
}
public void addArtifact(CheckSourceReleaseResult result) {
artifactsCount++;
if (!result.central.isEmpty()) {
centralMissing++;
}
if (result.dist == null) {
return;
}
if (!result.dist.isEmpty() || !result.distOlder.isEmpty()) {
distError++;
}
if (!result.dist.isEmpty()) {
distMissing++;
}
if (!result.distOlder.isEmpty()) {
distOlder++;
}
}
}
private void reportLine(Sink sink, CheckSourceReleaseResult csrr) {
ConfigurationLineInfo cli = csrr.getConfigurationLine();
sink.tableRow();
sink.tableCell();
sink.anchor(cli.getArtifactId());
sink.rawText(cli.getArtifactId());
sink.anchor_();
sink.tableCell_();
// LATEST column
sink.tableCell();
sink.link(cli.getMetadataFileURL(repoBaseUrl));
sink.rawText(csrr.getVersion());
sink.link_();
sink.tableCell_();
// DATE column
sink.tableCell();
sink.rawText(cli.getReleaseDateFromMetadata());
sink.tableCell_();
// dist column
sink.tableCell();
if (csrr.dist != null) {
if (cli.isSrcBin()) {
String directory = csrr.getVersion() + "/source/";
sink.link(distributionAreaUrl + cli.getDirectory() + '/' + directory);
sink.text(directory);
sink.link_();
}
if (csrr.dist.isEmpty() && csrr.distOlder.isEmpty()) {
sink.text(cli.getSourceReleaseFilename(csrr.getVersion(), true));
iconSuccess(sink);
}
StringBuilder cliMissing = new StringBuilder();
for (String missing : csrr.dist) {
sink.lineBreak();
iconError(sink);
sink.rawText(missing);
if (!csrr.central.contains(missing)) {
// if the release distribution is in central repository, we can get it from there...
cliMissing.append("\nwget ");
cliMissing.append(cli.getVersionnedFolderURL(repoBaseUrl, csrr.getVersion()));
cliMissing.append(missing);
cliMissing.append("\nsvn add ").append(missing);
}
}
if (!cliMissing.toString().isEmpty()) {
sink.lineBreak();
SinkEventAttributeSet atts = new SinkEventAttributeSet();
sink.unknown("pre", new Object[] {new Integer(HtmlMarkup.TAG_TYPE_START)}, atts);
sink.text(cliMissing.toString());
sink.unknown("pre", new Object[] {new Integer(HtmlMarkup.TAG_TYPE_END)}, null);
}
StringBuilder cliOlder = new StringBuilder();
for (String missing : csrr.distOlder) {
sink.lineBreak();
iconRemove(sink);
sink.rawText(missing);
cliOlder.append("\nsvn rm ").append(missing);
}
if (!cliOlder.toString().isEmpty()) {
sink.lineBreak();
SinkEventAttributeSet atts = new SinkEventAttributeSet();
sink.unknown("pre", new Object[] {new Integer(HtmlMarkup.TAG_TYPE_START)}, atts);
sink.text(cliOlder.toString());
sink.unknown("pre", new Object[] {new Integer(HtmlMarkup.TAG_TYPE_END)}, null);
}
}
// central column
sink.tableCell();
sink.link(cli.getBaseURL(repoBaseUrl, ""));
sink.text("<artifactId>");
sink.link_();
sink.text("/");
sink.link(cli.getVersionnedFolderURL(repoBaseUrl, csrr.getVersion()));
sink.text(csrr.getVersion());
sink.link_();
sink.text("/(source-release)");
if (csrr.central.isEmpty()) {
iconSuccess(sink);
} else {
iconWarning(sink);
}
for (String missing : csrr.central) {
sink.lineBreak();
iconError(sink);
sink.rawText(missing);
}
sink.tableCell_();
sink.tableCell_();
sink.tableRow_();
}
/** {@inheritDoc} */
@Override
protected void executeReport(Locale locale) throws MavenReportException {
if (!outputDirectory.exists()) {
outputDirectory.mkdirs();
}
try {
this.execute();
} catch (MojoExecutionException ex) {
throw new MavenReportException(ex.getMessage(), ex);
}
DirectoryStatistics stats = new DirectoryStatistics("", "org.apache.maven"); // global stats
List<DirectoryStatistics> statistics = new ArrayList<>();
DirectoryStatistics current = null;
for (CheckSourceReleaseResult csrr : results) {
if ((current == null) || !current.contains(csrr)) {
current = new DirectoryStatistics(
csrr.getConfigurationLine().getDirectory(),
csrr.getConfigurationLine().getGroupId());
statistics.add(current);
}
current.addArtifact(csrr);
stats.addArtifact(csrr);
}
Sink sink = getSink();
sink.head();
sink.title();
sink.text("Check source release");
sink.title_();
sink.head_();
sink.body();
sink.section1();
sink.paragraph();
sink.text("Check Source Release"
+ " (= <artifactId>-<version>-source-release.zip + .asc + .sha1 or .sha512) availability in:");
sink.paragraph_();
sink.list();
sink.listItem();
sink.text("Apache Maven distribution area: ");
sink.link(distributionAreaUrl);
sink.text(distributionAreaUrl);
sink.link_();
sink.listItem_();
sink.listItem();
sink.text("Maven central repository: ");
sink.link(repoBaseUrl);
sink.text(repoBaseUrl);
sink.link_();
sink.listItem_();
sink.list_();
sink.paragraph();
sink.text("Older artifacts exploration is Work In Progress...");
sink.paragraph_();
sink.section1_();
sink.table();
sink.tableRow();
sink.tableHeaderCell();
sink.rawText("groupId/artifactId: " + String.valueOf(stats.artifactsCount));
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.rawText("LATEST");
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.rawText("DATE");
sink.tableHeaderCell_();
reportStatisticsHeader(stats, sink);
sink.tableRow_();
Iterator<DirectoryStatistics> dirs = statistics.iterator();
current = null;
for (CheckSourceReleaseResult csrr : results) {
if ((current == null) || !current.contains(csrr)) {
current = dirs.next();
sink.tableRow();
sink.tableHeaderCell();
// shorten groupid
sink.rawText(csrr.getConfigurationLine().getGroupId().replaceAll("org.apache.maven", "o.a.m") + ": "
+ String.valueOf(current.artifactsCount));
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.rawText(" ");
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.rawText(" ");
sink.tableHeaderCell_();
reportStatisticsHeader(current, sink);
sink.tableRow_();
}
reportLine(sink, csrr);
}
sink.table_();
sink.body_();
sink.flush();
sink.close();
}
private void reportStatisticsHeader(DirectoryStatistics current, Sink sink) {
sink.tableHeaderCell();
if (!NOT_IN_DISTRIBUTION_AREA.equals(current.directory)) {
sink.link(distributionAreaUrl + current.directory);
sink.text("<dist-area>/" + current.directory);
sink.link_();
sink.rawText(": " + String.valueOf(current.artifactsCount - current.distError));
iconSuccess(sink);
if (current.distError > 0) {
sink.rawText("/" + String.valueOf(current.distError));
iconWarning(sink);
sink.rawText("= " + String.valueOf(current.distMissing));
iconError(sink);
sink.rawText("/" + String.valueOf(current.distOlder));
iconRemove(sink);
}
}
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.link(repoBaseUrl + current.groupId.replace('.', '/'));
sink.text("<central>/" + current.groupId.replace('.', '/').replace("org/apache/maven", "o/a/m"));
sink.link_();
sink.rawText(": " + String.valueOf(current.artifactsCount - current.centralMissing));
iconSuccess(sink);
if (current.centralMissing > 0) {
sink.rawText("/" + String.valueOf(current.centralMissing));
iconWarning(sink);
}
sink.tableHeaderCell_();
}
/**
* Report a pattern for an artifact source release.
*
* @param artifact artifact name
* @return regex
*/
protected static String getSourceReleasePattern(String artifact) {
/// not the safest
return "^" + artifact + "-[0-9].*source-release.*$";
}
private String cachedUrl;
private Document cachedDocument;
private Document read(String url) throws IOException {
if (url.startsWith(distributionAreaUrl)) {
// distribution area: cache content, since it is read multiple times
if (!url.equals(cachedUrl)) {
cachedUrl = url;
cachedDocument = JsoupRetry.get(url);
}
return cachedDocument;
} else {
return JsoupRetry.get(url);
}
}
private Elements selectLinks(String repourl) throws IOException {
try {
return read(repourl).select("a[href]");
} catch (IOException ioe) {
throw new IOException("IOException while reading " + repourl, ioe);
}
}
private List<String> checkContainsOld(String url, ConfigurationLineInfo cli, String version) throws IOException {
Elements links = selectLinks(url);
String sourceReleaseFilename = cli.getSourceReleaseFilename(version, true);
List<String> retrievedOldFiles = new LinkedList<>();
for (Element e : links) {
String art = e.attr("href");
if (art.matches(getSourceReleasePattern(cli.getArtifactId()))) {
String retrievedFile = e.attr("href");
if (!retrievedFile.startsWith(sourceReleaseFilename)) {
retrievedOldFiles.add(retrievedFile);
}
}
}
if (!retrievedOldFiles.isEmpty()) {
// write the following output in red so it's more readable in jenkins console
addErrorLine(
cli,
version,
ignoreDistFailures,
"Different version than " + version + " for " + cli.getArtifactId() + " available in " + url);
for (String sourceItem : retrievedOldFiles) {
addErrorLine(cli, version, ignoreDistFailures, " > " + sourceItem + " <");
}
}
return retrievedOldFiles;
}
/**
* Check that url points to a directory index containing expected release files
*
* @param url
* @param cli
* @param version
* @return missing files
* @throws IOException
*/
private List<String> checkDirectoryIndex(String url, ConfigurationLineInfo cli, String version, boolean dist)
throws IOException {
Set<String> retrievedFiles = new HashSet<>();
Elements links = selectLinks(url);
for (Element e : links) {
retrievedFiles.add(e.attr("href"));
}
String sourceReleaseFilename = cli.getSourceReleaseFilename(version, dist);
List<String> missingFiles = new ArrayList<>();
// require source release file
if (!retrievedFiles.contains(sourceReleaseFilename)) {
missingFiles.add(sourceReleaseFilename);
}
// require source release file signature (.asc)
if (!retrievedFiles.contains(sourceReleaseFilename + ".asc")) {
missingFiles.add(sourceReleaseFilename + ".asc");
}
// require source release file checksum (.sha1 or .sha512)
if (!(retrievedFiles.contains(sourceReleaseFilename + ".sha1")
|| retrievedFiles.contains(sourceReleaseFilename + ".sha512"))) {
missingFiles.add(sourceReleaseFilename + ".sha1 or .sha512");
}
if (!missingFiles.isEmpty()) {
boolean error = addErrorLine(
cli, version, ignoreDistFailures, "Missing file for " + cli.getArtifactId() + " in " + url);
for (String sourceItem : missingFiles) {
addErrorLine(cli, version, ignoreDistFailures, " > " + sourceItem + " <");
}
if (error) {
getLog().warn("==> when reading " + url + " got following hrefs: " + retrievedFiles);
getLog().warn(url + " = " + read(url));
}
}
return missingFiles;
}
/** {@inheritDoc} */
@Override
protected void checkArtifact(ConfigurationLineInfo configLine, String version) throws MojoExecutionException {
try {
CheckSourceReleaseResult result = new CheckSourceReleaseResult(configLine, version);
results.add(result);
// central
String centralUrl = configLine.getVersionnedFolderURL(repoBaseUrl, version);
result.setMissingCentralSourceRelease(checkDirectoryIndex(centralUrl, configLine, version, false));
if (NOT_IN_DISTRIBUTION_AREA.equals(configLine.getDirectory())) {
// no distribution check
return;
}
// dist
String distUrl = distributionAreaUrl
+ configLine.getDirectory()
+ (configLine.isSrcBin() ? ("/" + version + "/source") : "");
result.setMissingDistSourceRelease(checkDirectoryIndex(distUrl, configLine, version, true));
result.setDistOlderSourceRelease(checkContainsOld(distUrl, configLine, version));
} catch (IOException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
}
}
}