This is an automated email from the ASF dual-hosted git repository. sjaranowski pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/maven-dist-tool.git
The following commit(s) were added to refs/heads/master by this push: new a78fa5b Committers stats based ob ML a78fa5b is described below commit a78fa5bc7b7c4efbd568592bc7fcfee3e9740341 Author: Slawomir Jaranowski <s.jaranow...@gmail.com> AuthorDate: Sat Nov 16 21:02:48 2024 +0100 Committers stats based ob ML --- pom.xml | 27 ++ .../org/apache/maven/dist/tools/IconsUtils.java | 72 +++++ .../tools/committers/CommittersStatsReport.java | 324 +++++++++++++++++++++ .../maven/dist/tools/committers/MLStats.java | 135 +++++++++ .../maven/dist/tools/committers/MLStatsAnn.java | 45 +++ .../dist/tools/committers/MLStatsCommits.java | 47 +++ .../maven/dist/tools/committers/MLStatsIssues.java | 46 +++ .../maven/dist/tools/committers/MLStatsVotes.java | 47 +++ .../committers/MavenCommittersRepository.java | 187 ++++++++++++ src/main/resources/committers-names.properties | 22 ++ .../committers/MavenCommittersRepositoryTest.java | 84 ++++++ 11 files changed, 1036 insertions(+) diff --git a/pom.xml b/pom.xml index e7e4769..8d08956 100644 --- a/pom.xml +++ b/pom.xml @@ -154,6 +154,11 @@ <artifactId>github-api</artifactId> <version>1.129</version> </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-core</artifactId> + <version>2.17.2</version> + </dependency> <dependency> <groupId>org.codehaus.plexus</groupId> <artifactId>plexus-xml</artifactId> @@ -167,11 +172,28 @@ <artifactId>junit-jupiter-api</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <version>3.26.3</version> + </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-params</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.wiremock</groupId> + <artifactId>wiremock</artifactId> + <version>3.9.2</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <version>1.7.36</version> + <scope>test</scope> + </dependency> </dependencies> <build> @@ -190,6 +212,10 @@ </plugins> </pluginManagement> <plugins> + <plugin> + <groupId>org.eclipse.sisu</groupId> + <artifactId>sisu-maven-plugin</artifactId> + </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>animal-sniffer-maven-plugin</artifactId> @@ -275,6 +301,7 @@ <report>list-plugins-prerequisites</report> <report>list-master-jobs</report> <report>list-branches</report> + <report>committers-stats</report> <report>memory-check</report> </reports> </reportSet> diff --git a/src/main/java/org/apache/maven/dist/tools/IconsUtils.java b/src/main/java/org/apache/maven/dist/tools/IconsUtils.java new file mode 100644 index 0000000..dde7c44 --- /dev/null +++ b/src/main/java/org/apache/maven/dist/tools/IconsUtils.java @@ -0,0 +1,72 @@ +/* + * 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; + +import org.apache.maven.doxia.sink.Sink; + +/** + * Print icons in reports. + */ +public class IconsUtils { + + /** + * Utility class + */ + private IconsUtils() {} + + /** + * add an error icon. + * + * @param sink doxiasink + */ + public static void error(Sink sink) { + icon(sink, "icon_error_sml"); + } + + /** + * add a warning icon. + * + * @param sink doxiasink + */ + public static void warning(Sink sink) { + icon(sink, "icon_warning_sml"); + } + + /** + * add an success icon. + * + * @param sink doxiasink + */ + public static void success(Sink sink) { + icon(sink, "icon_success_sml"); + } + + /** + * add a "remove" icon. + * + * @param sink doxiasink + */ + public static void remove(Sink sink) { + icon(sink, "remove"); + } + + private static void icon(Sink sink, String level) { + sink.figureGraphics("images/" + level + ".gif"); + } +} diff --git a/src/main/java/org/apache/maven/dist/tools/committers/CommittersStatsReport.java b/src/main/java/org/apache/maven/dist/tools/committers/CommittersStatsReport.java new file mode 100644 index 0000000..23ce054 --- /dev/null +++ b/src/main/java/org/apache/maven/dist/tools/committers/CommittersStatsReport.java @@ -0,0 +1,324 @@ +/* + * 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.committers; + +import javax.inject.Inject; + +import java.time.LocalDate; +import java.time.Period; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.apache.maven.dist.tools.IconsUtils; +import org.apache.maven.dist.tools.committers.MavenCommittersRepository.Committer; +import org.apache.maven.doxia.sink.Sink; +import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet.Semantics; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.reporting.AbstractMavenReport; +import org.apache.maven.reporting.AbstractMavenReportRenderer; +import org.apache.maven.reporting.MavenReportException; + +/** + * Generate a Committers statistic + */ +@Mojo(name = "committers-stats", requiresProject = false) +public class CommittersStatsReport extends AbstractMavenReport { + + public static final int LAST_ACTIVITY_MONTHS_ERROR = 4 * 12; + + public static final int LAST_ACTIVITY_MONTHS_WARNING = 2 * 12; + + private final Map<String, MLStats> mlStats; + + private final MavenCommittersRepository mavenCommitters; + + @Inject + public CommittersStatsReport(Map<String, MLStats> mlStats, MavenCommittersRepository mavenCommitters) { + this.mlStats = mlStats; + this.mavenCommitters = mavenCommitters; + } + + enum ActivityLevel { + SUCCESS, + WARNING, + ERROR; + } + + class Renderer extends AbstractMavenReportRenderer { + + private final String title; + + /** + * Default constructor. + * + * @param sink the sink to use. + */ + Renderer(Sink sink, String title) { + super(sink); + this.title = title; + } + + @Override + public String getTitle() { + return title; + } + + @Override + protected void renderBody() { + Map<Committer, List<String>> committerStats = retrieveCommitterStats(); + + startSection("Committers Stats"); + paragraph("Committer statistics are based on the searching at public mailing lists"); + renderStatsTable(committerStats); + endSection(); + + startSection("Committers Stats - summary"); + renderStatsSummary(committerStats); + endSection(); + + startSection("Legend"); + renderActivityLegend(); + queryDescriptionLegend(); + endSection(); + } + + private Map<Committer, List<String>> retrieveCommitterStats() { + Map<Committer, List<String>> result = new LinkedHashMap<>(); + for (Committer committer : mavenCommitters.getCommitters()) { + List<String> lastDateList = mlStats.values().stream() + .map(ml -> ml.getLast(committer)) + .toList(); + result.put(committer, lastDateList); + } + return result; + } + + private void renderStatsTable(Map<Committer, List<String>> committerStats) { + + int[] justification = new int[mlStats.size() + 3]; + Arrays.fill(justification, Sink.JUSTIFY_CENTER); + justification[1] = Sink.JUSTIFY_LEFT; + justification[2] = Sink.JUSTIFY_LEFT; + startTable(justification, false); + + List<String> headers = new ArrayList<>(); + headers.add(""); + headers.add("ID"); + headers.add("Names"); + headers.addAll(mlStats.keySet()); + tableHeader(headers.toArray(String[]::new)); + + int lp = 1; + for (Map.Entry<Committer, List<String>> entry : committerStats.entrySet()) { + + Committer committer = entry.getKey(); + List<String> lastDateList = entry.getValue(); + + sink.tableRow(); + tableCell(String.valueOf(lp)); + + sink.tableCell(); + sink.text(committer.id(), committer.pmc() ? Semantics.BOLD : null); + printStatusIcon(getActivityLevel(lastDateList)); + sink.tableCell_(); + + tableCell(String.join(", ", committer.names())); + + lastDateList.forEach(this::tableCellWithLastDate); + + sink.tableRow_(); + lp++; + } + endTable(); + } + + private void renderStatsSummary(Map<Committer, List<String>> committerStats) { + + Map<ActivityLevel, Long> pmcs = committerStats.entrySet().stream() + .filter(entry -> entry.getKey().pmc()) + .collect(Collectors.groupingBy(entry -> getActivityLevel(entry.getValue()), Collectors.counting())); + + Map<ActivityLevel, Long> commiters = committerStats.entrySet().stream() + .filter(entry -> !entry.getKey().pmc()) + .collect(Collectors.groupingBy(entry -> getActivityLevel(entry.getValue()), Collectors.counting())); + + startTable( + new int[] {Sink.JUSTIFY_CENTER, Sink.JUSTIFY_CENTER, Sink.JUSTIFY_CENTER, Sink.JUSTIFY_CENTER}, + false); + tableHeader(new String[] {"Activity", "Commiters", "PMCs", "Total"}); + long committersTotal = 0; + long pcmsTotal = 0; + for (ActivityLevel level : ActivityLevel.values()) { + + Long committersCount = Optional.ofNullable(commiters.get(level)).orElse(0L); + Long pcmsCount = Optional.ofNullable(pmcs.get(level)).orElse(0L); + + sink.tableRow(); + sink.tableCell(); + printStatusIcon(level); + sink.tableCell_(); + tableCell(String.valueOf(committersCount)); + tableCell(String.valueOf(pcmsCount)); + tableCell(String.valueOf(committersCount + pcmsCount)); + sink.tableRow_(); + committersTotal += committersCount; + pcmsTotal += pcmsCount; + } + tableRow(new String[] { + "Total", + String.valueOf(committersTotal), + String.valueOf(pcmsTotal), + String.valueOf(committersTotal + pcmsTotal) + }); + endTable(); + } + + private void renderActivityLegend() { + startSection("Last activity status"); + sink.definitionList(); + + sink.definedTerm(); + IconsUtils.success(sink); + sink.definedTerm_(); + sink.definition(); + sink.text("last activity within last "); + boldText(String.valueOf(LAST_ACTIVITY_MONTHS_WARNING)); + sink.text(" months"); + sink.definition_(); + + sink.definedTerm(); + IconsUtils.warning(sink); + sink.definedTerm_(); + sink.definition(); + sink.text("last activity between "); + boldText(String.valueOf(LAST_ACTIVITY_MONTHS_WARNING)); + sink.text(" and "); + boldText(String.valueOf(LAST_ACTIVITY_MONTHS_ERROR)); + sink.text(" months"); + sink.definition_(); + + sink.definedTerm(); + IconsUtils.error(sink); + sink.definedTerm_(); + sink.definition(); + sink.text("last activity before than "); + boldText(String.valueOf(LAST_ACTIVITY_MONTHS_ERROR)); + sink.text(" months"); + sink.definition_(); + + sink.definitionList_(); + endSection(); + } + + private void queryDescriptionLegend() { + startSection("Query description"); + sink.definitionList(); + + for (Map.Entry<String, MLStats> entry : mlStats.entrySet()) { + sink.definedTerm(); + sink.text(entry.getKey()); + sink.definedTerm_(); + sink.definition(); + sink.text(entry.getValue().getQueryDescription()); + sink.definition_(); + } + + sink.definitionList_(); + endSection(); + } + + private void boldText(String text) { + sink.bold(); + sink.text(text); + sink.bold_(); + } + + @SuppressWarnings("checkstyle:MissingSwitchDefault") + private void printStatusIcon(ActivityLevel activityLevel) { + sink.text(" "); + switch (activityLevel) { + case ERROR -> IconsUtils.error(sink); + case WARNING -> IconsUtils.warning(sink); + case SUCCESS -> IconsUtils.success(sink); + } + } + + private ActivityLevel getActivityLevel(List<String> dates) { + return getActivityLevel(dates.stream() + .filter(date -> !"-".equals(date)) + .max(Comparator.naturalOrder()) + .orElse("-")); + } + + private ActivityLevel getActivityLevel(String date) { + long monthAgo = 99; + + if (date != null && !"-".equals(date)) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + LocalDate localDate = LocalDate.parse(date + "-01", dateTimeFormatter); + Period period = Period.between(localDate, LocalDate.now()); + monthAgo = period.getYears() * 12L + period.getMonths(); + } + + sink.text(" "); + if (monthAgo >= LAST_ACTIVITY_MONTHS_ERROR) { + return ActivityLevel.ERROR; + } else if (monthAgo >= LAST_ACTIVITY_MONTHS_WARNING) { + return ActivityLevel.WARNING; + } else { + return ActivityLevel.SUCCESS; + } + } + + private void tableCellWithLastDate(String date) { + sink.tableCell(); + sink.text(date); + printStatusIcon(getActivityLevel(date)); + sink.tableCell_(); + } + } + + @Override + protected void executeReport(Locale locale) throws MavenReportException { + new Renderer(getSink(), getName(locale)).render(); + } + + @Override + public String getOutputName() { + return "dist-tool-committers-stats"; + } + + @Override + public String getName(Locale locale) { + return "Dist Tool> Committers Stats"; + } + + @Override + public String getDescription(Locale locale) { + return ""; + } +} diff --git a/src/main/java/org/apache/maven/dist/tools/committers/MLStats.java b/src/main/java/org/apache/maven/dist/tools/committers/MLStats.java new file mode 100644 index 0000000..8db1138 --- /dev/null +++ b/src/main/java/org/apache/maven/dist/tools/committers/MLStats.java @@ -0,0 +1,135 @@ +/* + * 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.committers; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import org.apache.maven.dist.tools.committers.MavenCommittersRepository.Committer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.util.Map.entry; + +public abstract class MLStats { + + private final Logger log = LoggerFactory.getLogger(MLStats.class); + + private static final String ML_STATS_ADDRES = "https://lists.apache.org/api/stats.lua"; + + private static final Map<String, String> STANDARD_QUERY_PARAMS = Map.ofEntries( + entry("d", "lte=1d"), // for stats 1 day is enough + entry("domain", "maven.apache.org")); + + protected abstract String getQueryDescription(); + + protected abstract List<Map<String, String>> getQueryParamsList(Committer committer); + + public String getLast(Committer committer) { + + List<Map<String, String>> queryParamsList = getQueryParamsList(committer); + return queryParamsList.stream() + .map(this::prepareStatsURI) + .map(this::getLastFromML) + .filter(Optional::isPresent) + .map(Optional::get) + .max(Comparator.naturalOrder()) + .orElse("-"); + } + + private Optional<String> getLastFromML(URI statsURI) { + try (HttpClient client = HttpClient.newHttpClient()) { + HttpRequest request = HttpRequest.newBuilder() + .GET() + .header("Accept", "application/json") + .uri(statsURI) + .timeout(Duration.ofSeconds(60)) + .build(); + HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()); + Optional<String> last = parseLast(response.body()); + log.info("Query: {}, retunrs: {}", statsURI, last); + return last; + + } catch (IOException e) { + log.warn("Query: {}, error: {}", statsURI, e.getMessage()); + // try next one ... + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + return Optional.empty(); + } + + private Optional<String> parseLast(InputStream input) throws IOException { + JsonFactory factory = new JsonFactory(); + Integer lastYear = null; + Integer lastMonth = null; + + try (JsonParser parser = factory.createParser(input)) { + while ((lastYear == null || lastMonth == null) && parser.nextToken() != JsonToken.END_OBJECT) { + if (parser.currentToken() != JsonToken.VALUE_NUMBER_INT) { + continue; + } + String name = parser.currentName(); + switch (name) { + case "lastYear": + lastYear = parser.getValueAsInt(); + break; + case "lastMonth": + lastMonth = parser.getValueAsInt(); + break; + default: + // ignore + } + } + } + + if (lastYear != null && lastMonth != null) { + if (lastYear == 1970 && lastMonth == 1) { + return Optional.empty(); + } + return Optional.of(String.format("%04d-%02d", lastYear, lastMonth)); + } + return Optional.empty(); + } + + private URI prepareStatsURI(Map<String, String> queryParams) { + return URI.create(ML_STATS_ADDRES + "?" + + Stream.concat(STANDARD_QUERY_PARAMS.entrySet().stream(), queryParams.entrySet().stream()) + .map(entry -> + entry.getKey() + "=" + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)) + .collect(Collectors.joining("&"))); + } +} diff --git a/src/main/java/org/apache/maven/dist/tools/committers/MLStatsAnn.java b/src/main/java/org/apache/maven/dist/tools/committers/MLStatsAnn.java new file mode 100644 index 0000000..da60484 --- /dev/null +++ b/src/main/java/org/apache/maven/dist/tools/committers/MLStatsAnn.java @@ -0,0 +1,45 @@ +/* + * 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.committers; + +import javax.inject.Named; +import javax.inject.Singleton; + +import java.util.List; +import java.util.Map; + +import org.apache.maven.dist.tools.committers.MavenCommittersRepository.Committer; + +import static java.util.Map.entry; +import static java.util.Map.ofEntries; + +@Named("ANN") +@Singleton +public class MLStatsAnn extends MLStats { + + @Override + protected String getQueryDescription() { + return "list announce and header_from committerId + @apache.org"; + } + + @Override + protected List<Map<String, String>> getQueryParamsList(Committer committer) { + return List.of(ofEntries(entry("list", "announce"), entry("header_from", committer.id() + "@apache.org"))); + } +} diff --git a/src/main/java/org/apache/maven/dist/tools/committers/MLStatsCommits.java b/src/main/java/org/apache/maven/dist/tools/committers/MLStatsCommits.java new file mode 100644 index 0000000..360edf2 --- /dev/null +++ b/src/main/java/org/apache/maven/dist/tools/committers/MLStatsCommits.java @@ -0,0 +1,47 @@ +/* + * 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.committers; + +import javax.inject.Named; +import javax.inject.Singleton; + +import java.util.List; +import java.util.Map; + +import org.apache.maven.dist.tools.committers.MavenCommittersRepository.Committer; + +import static java.util.Map.entry; +import static java.util.Map.ofEntries; + +@Named("Commits") +@Singleton +public class MLStatsCommits extends MLStats { + + @Override + protected String getQueryDescription() { + return "list commits or site-commits and header_from committerId + @apache.org"; + } + + @Override + protected List<Map<String, String>> getQueryParamsList(Committer committer) { + return List.of( + ofEntries(entry("list", "commits"), entry("header_from", committer.id() + "@apache.org")), + ofEntries(entry("list", "site-commits"), entry("header_from", committer.id() + "@apache.org"))); + } +} diff --git a/src/main/java/org/apache/maven/dist/tools/committers/MLStatsIssues.java b/src/main/java/org/apache/maven/dist/tools/committers/MLStatsIssues.java new file mode 100644 index 0000000..7c5d8f4 --- /dev/null +++ b/src/main/java/org/apache/maven/dist/tools/committers/MLStatsIssues.java @@ -0,0 +1,46 @@ +/* + * 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.committers; + +import javax.inject.Named; +import javax.inject.Singleton; + +import java.util.List; +import java.util.Map; + +import org.apache.maven.dist.tools.committers.MavenCommittersRepository.Committer; + +import static java.util.Map.entry; +import static java.util.Map.ofEntries; + +@Named("Issues") +@Singleton +public class MLStatsIssues extends MLStats { + + protected String getQueryDescription() { + return "list issues and header_from committer name"; + } + + @Override + protected List<Map<String, String>> getQueryParamsList(Committer committer) { + return committer.names().stream() + .map(name -> ofEntries(entry("list", "issues"), entry("header_from", name))) + .toList(); + } +} diff --git a/src/main/java/org/apache/maven/dist/tools/committers/MLStatsVotes.java b/src/main/java/org/apache/maven/dist/tools/committers/MLStatsVotes.java new file mode 100644 index 0000000..ed3b6b3 --- /dev/null +++ b/src/main/java/org/apache/maven/dist/tools/committers/MLStatsVotes.java @@ -0,0 +1,47 @@ +/* + * 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.committers; + +import javax.inject.Named; +import javax.inject.Singleton; + +import java.util.List; +import java.util.Map; + +import org.apache.maven.dist.tools.committers.MavenCommittersRepository.Committer; + +import static java.util.Map.entry; +import static java.util.Map.ofEntries; + +@Named("Votes") +@Singleton +public class MLStatsVotes extends MLStats { + + protected String getQueryDescription() { + return "list dev and header_subject [VOTE] and header_from committer name"; + } + + @Override + protected List<Map<String, String>> getQueryParamsList(Committer committer) { + return committer.names().stream() + .map(name -> + ofEntries(entry("list", "dev"), entry("header_subject", "[VOTE]"), entry("header_from", name))) + .toList(); + } +} diff --git a/src/main/java/org/apache/maven/dist/tools/committers/MavenCommittersRepository.java b/src/main/java/org/apache/maven/dist/tools/committers/MavenCommittersRepository.java new file mode 100644 index 0000000..8448457 --- /dev/null +++ b/src/main/java/org/apache/maven/dist/tools/committers/MavenCommittersRepository.java @@ -0,0 +1,187 @@ +/* + * 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.committers; + +import javax.inject.Named; +import javax.inject.Singleton; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.TreeMap; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Fetch a Maven committers + */ +@Named +@Singleton +public class MavenCommittersRepository { + + private static final Logger LOG = LoggerFactory.getLogger(MavenCommittersRepository.class); + + public record Committer(String id, List<String> names, boolean pmc) { + Committer(String id, boolean pmc) { + this(id, new ArrayList<>(), pmc); + } + } + + private static final String ASF_PROJECT_URL = "https://projects.apache.org"; + + private static final String ASF_GROUP_FILE = "/json/foundation/groups.json"; + + private static final String ASF_PEOPLE_FILE = "/json/foundation/people_name.json"; + + private final Map<String, Committer> committers = new TreeMap<>(); + + private final String asfProjectUrl; + + MavenCommittersRepository() { + this(ASF_PROJECT_URL); + } + + MavenCommittersRepository(String asfProjectUrl) { + this.asfProjectUrl = asfProjectUrl; + try { + loadData(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public Collection<Committer> getCommitters() { + return committers.values(); + } + + private void loadData() throws IOException { + + try (HttpClient client = HttpClient.newHttpClient()) { + HttpRequest request = HttpRequest.newBuilder() + .GET() + .header("Accept", "application/json") + .uri(URI.create(asfProjectUrl + ASF_GROUP_FILE)) + .timeout(Duration.ofSeconds(60)) + .build(); + + LOG.info("Loading Maven groups"); + HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()); + loadMavenGroup(response.body()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + try (HttpClient client = HttpClient.newHttpClient()) { + HttpRequest request = HttpRequest.newBuilder() + .GET() + .header("Accept", "application/json") + .uri(URI.create(asfProjectUrl + ASF_PEOPLE_FILE)) + .timeout(Duration.ofSeconds(60)) + .build(); + + LOG.info("Loading Committers names"); + HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()); + loadPeopleName(response.body()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + loadPeopleNameSupplement(); + } + + private void loadPeopleNameSupplement() throws IOException { + Properties props = new Properties(); + + try (Reader in = new InputStreamReader( + getClass().getResourceAsStream("/committers-names.properties"), StandardCharsets.UTF_8)) { + props.load(in); + } + + for (String key : props.stringPropertyNames()) { + Committer committer = committers.get(key); + if (committer != null) { + Arrays.stream(props.getProperty(key).split(",")) + .map(String::trim) + .forEach(name -> committer.names.add(name.trim())); + } + } + } + + private void loadMavenGroup(InputStream input) throws IOException { + + List<String> ids = new ArrayList<>(); + List<String> pmcs = new ArrayList<>(); + + try (JsonParser parser = new JsonFactory().createParser(input)) { + while (parser.nextToken() != JsonToken.END_OBJECT && (ids.isEmpty() || pmcs.isEmpty())) { + if ("maven".equals(parser.currentName()) && parser.currentToken() == JsonToken.START_ARRAY) { + while (parser.nextToken() != JsonToken.END_ARRAY) { + ids.add(parser.getText()); + } + } + if ("maven-pmc".equals(parser.currentName()) && parser.currentToken() == JsonToken.START_ARRAY) { + while (parser.nextToken() != JsonToken.END_ARRAY) { + pmcs.add(parser.getText()); + } + } + } + } + + ids.stream() + .map(id -> new Committer(id, pmcs.contains(id))) + .forEach(committer -> committers.put(committer.id, committer)); + } + + private void loadPeopleName(InputStream input) throws IOException { + int itemFounds = 0; + try (JsonParser parser = new JsonFactory().createParser(input)) { + while (parser.nextToken() != JsonToken.END_OBJECT && itemFounds < committers.size()) { + if (parser.currentToken() == JsonToken.VALUE_STRING) { + String id = parser.currentName(); + String name = parser.getText(); + Committer committer = committers.get(id); + if (committer != null) { + committer.names.add(name); + itemFounds++; + } + } + } + } + } +} diff --git a/src/main/resources/committers-names.properties b/src/main/resources/committers-names.properties new file mode 100644 index 0000000..8d06c2e --- /dev/null +++ b/src/main/resources/committers-names.properties @@ -0,0 +1,22 @@ +# 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. + +# additional names used on ML by committers + +brianf = Brian Fox +cstamas = Tamás Cservenák +hboutemy = Hervé Boutemy diff --git a/src/test/java/org/apache/maven/dist/tools/committers/MavenCommittersRepositoryTest.java b/src/test/java/org/apache/maven/dist/tools/committers/MavenCommittersRepositoryTest.java new file mode 100644 index 0000000..9b9625d --- /dev/null +++ b/src/test/java/org/apache/maven/dist/tools/committers/MavenCommittersRepositoryTest.java @@ -0,0 +1,84 @@ +/* + * 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.committers; + +import java.util.List; + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import org.apache.maven.dist.tools.committers.MavenCommittersRepository.Committer; +import org.junit.jupiter.api.Test; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static org.assertj.core.api.Assertions.assertThat; + +@WireMockTest +class MavenCommittersRepositoryTest { + + private static final String GROUP = + """ + { + "test1": [ + "test1", + "test2" + ], + "maven": [ + "m1", + "m2", + "cstamas" + ], + "maven-pmc": [ + "cstamas" + ] + } + """; + + private static final String NAMES = + """ + { + "test1": "test 1 name", + "m1": "M1 name", + "test2": "test 2 name", + "m2": "M2 name", + "test3": "test 3 name", + "cstamas": "Tamas Cservenak", + "test4": "test 4 name", + "test5": "test 5 name", + } + """; + + @Test + void testDataLoad(WireMockRuntimeInfo wireMockRuntimeInfo) { + + stubFor(get("/json/foundation/groups.json") + .willReturn(aResponse().withStatus(200).withBody(GROUP))); + stubFor(get("/json/foundation/people_name.json") + .willReturn(aResponse().withStatus(200).withBody(NAMES))); + + MavenCommittersRepository mavenCommittersRepository = + new MavenCommittersRepository(wireMockRuntimeInfo.getHttpBaseUrl()); + assertThat(mavenCommittersRepository.getCommitters()) + .containsExactly( + new Committer("cstamas", List.of("Tamas Cservenak", "Tamás Cservenák"), true), + new Committer("m1", List.of("M1 name"), false), + new Committer("m2", List.of("M2 name"), false)); + } +}