Skip to content

Commit a42a6b2

Browse files
committed
Maintenance score based on commit frequency
1 parent 40d4517 commit a42a6b2

27 files changed

Lines changed: 1808 additions & 5 deletions

pom.xml

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@
4545

4646
<properties>
4747
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
48-
<maven.compiler.release>17</maven.compiler.release>
48+
<maven.compiler.release>21</maven.compiler.release>
4949

50-
<jackson-jr-objects.version>2.18.1</jackson-jr-objects.version>
50+
<jackson.version>2.18.1</jackson.version>
5151
<junit-jupiter.version>5.11.3</junit-jupiter.version>
5252
<assertj-core.version>3.26.3</assertj-core.version>
5353
<mockito-junit-jupiter.version>5.14.2</mockito-junit-jupiter.version>
@@ -127,6 +127,13 @@
127127
<version>${maven-dependencies.version}</version>
128128
<scope>provided</scope>
129129
</dependency>
130+
<dependency>
131+
<groupId>com.fasterxml.jackson</groupId>
132+
<artifactId>jackson-bom</artifactId>
133+
<version>${jackson.version}</version>
134+
<scope>import</scope>
135+
<type>pom</type>
136+
</dependency>
130137
</dependencies>
131138
</dependencyManagement>
132139

@@ -143,13 +150,14 @@
143150
<version>${maven-plugin-tools.version}</version>
144151
<scope>provided</scope>
145152
</dependency>
146-
147153
<dependency>
148154
<groupId>com.fasterxml.jackson.jr</groupId>
149155
<artifactId>jackson-jr-objects</artifactId>
150-
<version>${jackson-jr-objects.version}</version>
151156
</dependency>
152-
157+
<dependency>
158+
<groupId>com.fasterxml.jackson.jr</groupId>
159+
<artifactId>jackson-jr-annotation-support</artifactId>
160+
</dependency>
153161
<dependency>
154162
<groupId>org.junit.jupiter</groupId>
155163
<artifactId>junit-jupiter</artifactId>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.giovds;
2+
3+
import com.giovds.dto.PomResponse;
4+
import com.giovds.dto.Scm;
5+
import org.apache.maven.plugin.logging.Log;
6+
import org.w3c.dom.Document;
7+
import org.w3c.dom.Element;
8+
import org.w3c.dom.NodeList;
9+
import org.xml.sax.SAXException;
10+
11+
import javax.xml.parsers.DocumentBuilder;
12+
import javax.xml.parsers.DocumentBuilderFactory;
13+
import javax.xml.parsers.ParserConfigurationException;
14+
import java.io.IOException;
15+
import java.io.InputStream;
16+
import java.net.URI;
17+
import java.net.http.HttpClient;
18+
import java.net.http.HttpRequest;
19+
import java.net.http.HttpResponse;
20+
import java.nio.charset.StandardCharsets;
21+
22+
public class PomClient implements PomClientInterface {
23+
24+
private final String basePath;
25+
private final String pomPathTemplate;
26+
27+
private final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
28+
29+
private final HttpClient client = HttpClient.newBuilder()
30+
.version(HttpClient.Version.HTTP_2)
31+
.build();
32+
33+
private final Log log;
34+
35+
public PomClient(Log log) {
36+
this("https://repo1.maven.org", "/maven2/%s/%s/%s/%s-%s.pom", log);
37+
}
38+
39+
public PomClient(String basePath, String pomPathTemplate, Log log) {
40+
this.basePath = basePath;
41+
this.pomPathTemplate = pomPathTemplate;
42+
this.log = log;
43+
}
44+
45+
public PomResponse getPom(String group, String artifact, String version) throws IOException, InterruptedException {
46+
final String path = String.format(pomPathTemplate, group.replace(".", "/"), artifact, version, artifact, version);
47+
final HttpRequest request = HttpRequest.newBuilder()
48+
.GET()
49+
.uri(URI.create(basePath + path))
50+
.build();
51+
52+
return client.send(request, new PomResponseBodyHandler()).body();
53+
}
54+
55+
private class PomResponseBodyHandler implements HttpResponse.BodyHandler<PomResponse> {
56+
57+
@Override
58+
public HttpResponse.BodySubscriber<PomResponse> apply(final HttpResponse.ResponseInfo responseInfo) {
59+
int statusCode = responseInfo.statusCode();
60+
61+
if (statusCode < 200 || statusCode >= 300) {
62+
return HttpResponse.BodySubscribers.mapping(HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8), s -> {
63+
throw new RuntimeException("Search failed: status: %d body: %s".formatted(responseInfo.statusCode(), s));
64+
});
65+
}
66+
67+
HttpResponse.BodySubscriber<InputStream> stream = HttpResponse.BodySubscribers.ofInputStream();
68+
69+
return HttpResponse.BodySubscribers.mapping(stream, this::toPomResponse);
70+
}
71+
72+
private PomResponse toPomResponse(final InputStream inputStream) {
73+
try (final InputStream input = inputStream) {
74+
DocumentBuilder documentBuilder = PomClient.this.documentBuilderFactory.newDocumentBuilder();
75+
Document doc = documentBuilder.parse(input);
76+
77+
doc.getDocumentElement().normalize();
78+
79+
Element root = doc.getDocumentElement();
80+
NodeList urlNodes = root.getElementsByTagName("url");
81+
82+
if (urlNodes.getLength() == 0) {
83+
return PomResponse.empty();
84+
}
85+
String url = urlNodes.item(0).getTextContent();
86+
87+
Scm scm = Scm.empty();
88+
NodeList scmNodes = root.getElementsByTagName("scm");
89+
if (scmNodes.getLength() > 0) {
90+
Element scmElement = (Element) scmNodes.item(0);
91+
NodeList scmUrlNodes = scmElement.getElementsByTagName("url");
92+
if (scmUrlNodes.getLength() > 0) {
93+
String scmUrl = scmUrlNodes.item(0).getTextContent();
94+
scm = new Scm(scmUrl);
95+
}
96+
}
97+
98+
return new PomResponse(url, scm);
99+
} catch (IOException | ParserConfigurationException | SAXException e) {
100+
throw new RuntimeException(e);
101+
}
102+
}
103+
}
104+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.giovds;
2+
3+
import com.giovds.dto.PomResponse;
4+
5+
import java.io.IOException;
6+
7+
public interface PomClientInterface {
8+
PomResponse getPom(String group, String artifact, String version) throws IOException, InterruptedException;
9+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package com.giovds;
2+
3+
import com.giovds.collector.github.GithubCollector;
4+
import com.giovds.collector.github.GithubGuesser;
5+
import com.giovds.dto.PomResponse;
6+
import com.giovds.dto.github.internal.Collected;
7+
import com.giovds.evaluator.MaintenanceEvaluator;
8+
import org.apache.maven.model.Dependency;
9+
import org.apache.maven.plugin.AbstractMojo;
10+
import org.apache.maven.plugin.MojoFailureException;
11+
import org.apache.maven.plugin.logging.Log;
12+
import org.apache.maven.plugin.logging.SystemStreamLog;
13+
import org.apache.maven.plugins.annotations.LifecyclePhase;
14+
import org.apache.maven.plugins.annotations.Mojo;
15+
import org.apache.maven.plugins.annotations.Parameter;
16+
import org.apache.maven.plugins.annotations.ResolutionScope;
17+
import org.apache.maven.project.MavenProject;
18+
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.concurrent.ExecutionException;
22+
import java.util.stream.Collectors;
23+
24+
@Mojo(name = "unmaintained",
25+
defaultPhase = LifecyclePhase.TEST_COMPILE,
26+
requiresOnline = true,
27+
requiresDependencyResolution = ResolutionScope.TEST)
28+
public class UnmaintainedMojo extends AbstractMojo {
29+
30+
private final PomClientInterface client;
31+
private final GithubGuesser githubGuesser;
32+
private final GithubCollector githubCollector;
33+
private final MaintenanceEvaluator maintenanceEvaluator;
34+
35+
@Parameter(readonly = true, required = true, defaultValue = "${project}")
36+
private MavenProject project;
37+
38+
/**
39+
* Required for initialization by Maven
40+
*/
41+
public UnmaintainedMojo() {
42+
this(new SystemStreamLog());
43+
}
44+
45+
public UnmaintainedMojo(Log log) {
46+
this(new PomClient(log), new GithubGuesser(), new GithubCollector(log), new MaintenanceEvaluator());
47+
}
48+
49+
public UnmaintainedMojo(
50+
final PomClientInterface client,
51+
final GithubGuesser githubGuesser,
52+
final GithubCollector githubCollector,
53+
final MaintenanceEvaluator maintenanceEvaluator) {
54+
this.client = client;
55+
this.githubGuesser = githubGuesser;
56+
this.githubCollector = githubCollector;
57+
this.maintenanceEvaluator = maintenanceEvaluator;
58+
}
59+
60+
@Override
61+
public void execute() throws MojoFailureException {
62+
final List<Dependency> dependencies = project.getDependencies();
63+
64+
if (dependencies.isEmpty()) {
65+
// When building a POM without any dependencies there will be nothing to query.
66+
return;
67+
}
68+
69+
final Map<Dependency, PomResponse> pomResponses = dependencies.stream()
70+
.map(dependency -> {
71+
try {
72+
PomResponse pomResponse = client.getPom(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion());
73+
74+
return new DependencyPomResponsePair(dependency, pomResponse);
75+
} catch (Exception e) {
76+
getLog().error("Failed to fetch POM for %s:%s:%s".formatted(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion()), e);
77+
return new DependencyPomResponsePair(dependency, PomResponse.empty());
78+
}
79+
})
80+
.collect(Collectors.toMap(DependencyPomResponsePair::dependency, DependencyPomResponsePair::pomResponse));
81+
82+
for (Dependency dependency : pomResponses.keySet()) {
83+
final PomResponse pomResponse = pomResponses.get(dependency);
84+
final String projectUrl = pomResponse.url();
85+
final String projectScmUrl = pomResponse.scmUrl();
86+
87+
// First try to get the Github owner and repo from the url otherwise try to get it from the SCM url
88+
var guess = projectUrl != null ? githubGuesser.guess(projectUrl) : null;
89+
if (guess == null && projectScmUrl != null) {
90+
guess = githubGuesser.guess(projectScmUrl);
91+
}
92+
93+
if (guess == null) {
94+
getLog().warn("Could not guess Github owner and repo for %s".formatted(dependency.getManagementKey()));
95+
continue;
96+
}
97+
98+
Collected collected;
99+
try {
100+
collected = githubCollector.collect(guess.owner(), guess.repo());
101+
} catch (ExecutionException | InterruptedException e) {
102+
throw new MojoFailureException("Failed to collect Github data for %s".formatted(dependency.getManagementKey()), e);
103+
}
104+
105+
double score = maintenanceEvaluator.evaluateCommitsFrequency(collected);
106+
getLog().info("Maintenance score for %s: %f".formatted(dependency.getManagementKey(), score));
107+
}
108+
}
109+
110+
private record DependencyPomResponsePair(Dependency dependency, PomResponse pomResponse) {
111+
}
112+
}

0 commit comments

Comments
 (0)