Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mojo that calculates the maintenance score for dependencies hosted in Github #80

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
<maven.compiler.release>21</maven.compiler.release>

<jackson-jr-objects.version>2.18.1</jackson-jr-objects.version>
<jackson.version>2.18.1</jackson.version>
<junit-jupiter.version>5.11.3</junit-jupiter.version>
<assertj-core.version>3.26.3</assertj-core.version>
<mockito-junit-jupiter.version>5.14.2</mockito-junit-jupiter.version>
Expand Down Expand Up @@ -127,6 +127,13 @@
<version>${maven-dependencies.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>${jackson.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>

Expand All @@ -143,13 +150,14 @@
<version>${maven-plugin-tools.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.jr</groupId>
<artifactId>jackson-jr-objects</artifactId>
<version>${jackson-jr-objects.version}</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.jr</groupId>
<artifactId>jackson-jr-annotation-support</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
Expand Down
104 changes: 104 additions & 0 deletions src/main/java/com/giovds/PomClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.giovds;

import com.giovds.dto.PomResponse;
import com.giovds.dto.Scm;
import org.apache.maven.plugin.logging.Log;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;

public class PomClient implements PomClientInterface {

private final String basePath;
private final String pomPathTemplate;

private final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();

private final HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.build();

private final Log log;

public PomClient(Log log) {
this("https://repo1.maven.org", "/maven2/%s/%s/%s/%s-%s.pom", log);
}

public PomClient(String basePath, String pomPathTemplate, Log log) {
this.basePath = basePath;
this.pomPathTemplate = pomPathTemplate;
this.log = log;
}

public PomResponse getPom(String group, String artifact, String version) throws IOException, InterruptedException {
final String path = String.format(pomPathTemplate, group.replace(".", "/"), artifact, version, artifact, version);
final HttpRequest request = HttpRequest.newBuilder()
.GET()
.uri(URI.create(basePath + path))
.build();

return client.send(request, new PomResponseBodyHandler()).body();
}

private class PomResponseBodyHandler implements HttpResponse.BodyHandler<PomResponse> {

@Override
public HttpResponse.BodySubscriber<PomResponse> apply(final HttpResponse.ResponseInfo responseInfo) {
int statusCode = responseInfo.statusCode();

if (statusCode < 200 || statusCode >= 300) {
return HttpResponse.BodySubscribers.mapping(HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8), s -> {
throw new RuntimeException("Search failed: status: %d body: %s".formatted(responseInfo.statusCode(), s));
});
}

HttpResponse.BodySubscriber<InputStream> stream = HttpResponse.BodySubscribers.ofInputStream();

return HttpResponse.BodySubscribers.mapping(stream, this::toPomResponse);
}

private PomResponse toPomResponse(final InputStream inputStream) {
try (final InputStream input = inputStream) {
DocumentBuilder documentBuilder = PomClient.this.documentBuilderFactory.newDocumentBuilder();
Document doc = documentBuilder.parse(input);

doc.getDocumentElement().normalize();

Element root = doc.getDocumentElement();
NodeList urlNodes = root.getElementsByTagName("url");

if (urlNodes.getLength() == 0) {
return PomResponse.empty();
}
String url = urlNodes.item(0).getTextContent();

Scm scm = Scm.empty();
NodeList scmNodes = root.getElementsByTagName("scm");
if (scmNodes.getLength() > 0) {
Element scmElement = (Element) scmNodes.item(0);
NodeList scmUrlNodes = scmElement.getElementsByTagName("url");
if (scmUrlNodes.getLength() > 0) {
String scmUrl = scmUrlNodes.item(0).getTextContent();
scm = new Scm(scmUrl);
}
}

return new PomResponse(url, scm);
} catch (IOException | ParserConfigurationException | SAXException e) {
throw new RuntimeException(e);
}
}
}
}
9 changes: 9 additions & 0 deletions src/main/java/com/giovds/PomClientInterface.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.giovds;

import com.giovds.dto.PomResponse;

import java.io.IOException;

public interface PomClientInterface {
PomResponse getPom(String group, String artifact, String version) throws IOException, InterruptedException;
}
112 changes: 112 additions & 0 deletions src/main/java/com/giovds/UnmaintainedMojo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.giovds;

import com.giovds.collector.github.GithubCollector;
import com.giovds.collector.github.GithubGuesser;
import com.giovds.dto.PomResponse;
import com.giovds.dto.github.internal.Collected;
import com.giovds.evaluator.MaintenanceEvaluator;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugin.logging.SystemStreamLog;
import org.apache.maven.plugins.annotations.LifecyclePhase;
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.MavenProject;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

@Mojo(name = "unmaintained",
defaultPhase = LifecyclePhase.TEST_COMPILE,
requiresOnline = true,
requiresDependencyResolution = ResolutionScope.TEST)
public class UnmaintainedMojo extends AbstractMojo {

private final PomClientInterface client;
private final GithubGuesser githubGuesser;
private final GithubCollector githubCollector;
private final MaintenanceEvaluator maintenanceEvaluator;

@Parameter(readonly = true, required = true, defaultValue = "${project}")
private MavenProject project;

/**
* Required for initialization by Maven
*/
public UnmaintainedMojo() {
this(new SystemStreamLog());
}

public UnmaintainedMojo(Log log) {
this(new PomClient(log), new GithubGuesser(), new GithubCollector(log), new MaintenanceEvaluator());
}

public UnmaintainedMojo(
final PomClientInterface client,
final GithubGuesser githubGuesser,
final GithubCollector githubCollector,
final MaintenanceEvaluator maintenanceEvaluator) {
this.client = client;
this.githubGuesser = githubGuesser;
this.githubCollector = githubCollector;
this.maintenanceEvaluator = maintenanceEvaluator;
}

@Override
public void execute() throws MojoFailureException {
final List<Dependency> dependencies = project.getDependencies();

if (dependencies.isEmpty()) {
// When building a POM without any dependencies there will be nothing to query.
return;
}

final Map<Dependency, PomResponse> pomResponses = dependencies.stream()
.map(dependency -> {
try {
PomResponse pomResponse = client.getPom(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion());

return new DependencyPomResponsePair(dependency, pomResponse);
} catch (Exception e) {
getLog().error("Failed to fetch POM for %s:%s:%s".formatted(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion()), e);
return new DependencyPomResponsePair(dependency, PomResponse.empty());
}
})
.collect(Collectors.toMap(DependencyPomResponsePair::dependency, DependencyPomResponsePair::pomResponse));

for (Dependency dependency : pomResponses.keySet()) {
final PomResponse pomResponse = pomResponses.get(dependency);
final String projectUrl = pomResponse.url();
final String projectScmUrl = pomResponse.scmUrl();

// First try to get the Github owner and repo from the url otherwise try to get it from the SCM url
var guess = projectUrl != null ? githubGuesser.guess(projectUrl) : null;
if (guess == null && projectScmUrl != null) {
guess = githubGuesser.guess(projectScmUrl);
}

if (guess == null) {
getLog().warn("Could not guess Github owner and repo for %s".formatted(dependency.getManagementKey()));
continue;
}

Collected collected;
try {
collected = githubCollector.collect(guess.owner(), guess.repo());
} catch (ExecutionException | InterruptedException e) {
throw new MojoFailureException("Failed to collect Github data for %s".formatted(dependency.getManagementKey()), e);
}

double score = maintenanceEvaluator.evaluateCommitsFrequency(collected);
getLog().info("Maintenance score for %s: %f".formatted(dependency.getManagementKey(), score));
}
}

private record DependencyPomResponsePair(Dependency dependency, PomResponse pomResponse) {
}
}
Loading