Skip to content
Merged
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
9 changes: 9 additions & 0 deletions bom/platform-common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<version.bom.jakarta.transaction>2.0.1</version.bom.jakarta.transaction>
<version.bom.jakarta.xml.bind>4.0.2</version.bom.jakarta.xml.bind>
<version.bom.org.jberet>3.1.0.Final</version.bom.org.jberet>
<version.bom.com.fasterxml.jackson>2.20.0</version.bom.com.fasterxml.jackson>
</properties>

<dependencyManagement>
Expand Down Expand Up @@ -245,6 +246,14 @@
<scope>import</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>${version.bom.com.fasterxml.jackson}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!-- Dependencies with no BOMs -->

<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import static org.hibernate.search.build.enforcer.MavenProjectUtils.isProjectDeploySkipped;

import java.io.IOException;
import java.io.InputStreamReader;
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.util.ArrayList;
import java.util.HashSet;
Expand All @@ -30,7 +32,6 @@

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.stream.JsonReader;

import org.apache.maven.enforcer.rule.api.AbstractEnforcerRule;
import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
Expand All @@ -44,7 +45,9 @@ public class DependencyManagementIncludesAllGroupIdArtifactsRule extends Abstrac
* See <a href="https://central.sonatype.org/search/rest-api-guide/">Maven Central REST API</a>
*/
private static final String BASE_URL_FORMAT =
"https://search.maven.org/solrsearch/select?q=%s&core=gav&start=%d&rows=%d&wt=json";
"https://central.sonatype.com/solrsearch/select?q=%s&core=gav&start=%d&rows=%d&wt=json";
// This one here is more for a test purpose rather than to be used in "prod":
private static final String CENTRAL_SEARCH_INTERNAL_URL = "https://central.sonatype.com/api/internal/browse/components";
private static final String MAVEN_STATUS_URL = "https://status.maven.org/api/v2/summary.json";
private static final int ROWS_PER_PAGE = 100;
private static final int MAX_RETRIES = 5;
Expand All @@ -71,6 +74,8 @@ public class DependencyManagementIncludesAllGroupIdArtifactsRule extends Abstrac
*/
private Set<Dependency> dependenciesToSkip = new HashSet<>();

private HttpClient client = HttpClient.newBuilder().build();

public void execute() throws EnforcerRuleException {
Set<String> dependencies = session.getCurrentProject()
.getDependencyManagement()
Expand Down Expand Up @@ -123,27 +128,7 @@ public void execute() throws EnforcerRuleException {

if ( mavenStatus.isSearchAvailable() ) {
for ( Artifact filter : toQuery ) {
StringBuilder queryBuilder = new StringBuilder();
queryBuilder.append( "g:" ).append( encodeValue( filter.g ) );

if ( filter.a != null && !filter.a.trim().isEmpty() ) {
queryBuilder.append( "+AND+a:" ).append( encodeValue( filter.a ) );
}
if ( filter.v != null && !filter.v.trim().isEmpty() ) {
queryBuilder.append( "+AND+v:" ).append( encodeValue( filter.v ) );
}

int start = 0;
do {
String url = String.format( Locale.ROOT, BASE_URL_FORMAT, queryBuilder, start, ROWS_PER_PAGE );
SearchResponse response = fetch( gson, url, SearchResponse.class, SearchResponse::empty );
foundArtifacts.addAll( response.response.docs );
if ( response.response.start + response.response.docs.size() >= response.response.numFound ) {
break;
}
start += ROWS_PER_PAGE;
}
while ( true );
mavenCentralSolrSearch( filter, gson, foundArtifacts );
}
}
else {
Expand All @@ -167,6 +152,48 @@ public void execute() throws EnforcerRuleException {
}
}

private void mavenCentralInternalSearch(Artifact filter, Gson gson, Set<Artifact> foundArtifacts)
throws EnforcerRuleException {
CentralSearchRequest request = new CentralSearchRequest( filter );
getLog().info( "Fetching information for " + request );
do {
CentralSearchResponse response =
post( gson, client, CENTRAL_SEARCH_INTERNAL_URL, request, CentralSearchResponse.class,
CentralSearchResponse::empty );
foundArtifacts.addAll( response.components.stream().map( Artifact::new ).toList() );
if ( request.nextPage() >= response.pageCount ) {
break;
}
}
while ( true );
}

private void mavenCentralSolrSearch(Artifact filter, Gson gson, Set<Artifact> foundArtifacts) throws EnforcerRuleException {
StringBuilder queryBuilder = new StringBuilder();
queryBuilder.append( "g:" ).append( encodeValue( filter.g ) );

if ( filter.a != null && !filter.a.trim().isEmpty() ) {
queryBuilder.append( "+AND+a:" ).append( encodeValue( filter.a ) );
}
if ( filter.v != null && !filter.v.trim().isEmpty() ) {
queryBuilder.append( "+AND+v:" ).append( encodeValue( filter.v ) );
}

int start = 0;
int collectedSoFar = 0;
do {
String url = String.format( Locale.ROOT, BASE_URL_FORMAT, queryBuilder, start, ROWS_PER_PAGE );
SearchResponse response = fetch( gson, url, SearchResponse.class, SearchResponse::empty );
collectedSoFar += response.response.docs.size();
foundArtifacts.addAll( response.response.docs );
if ( collectedSoFar == response.response.numFound || response.response.docs.isEmpty() ) {
break;
}
start++;
}
while ( true );
}

private static String dependencyString(Dependency d) {
return String.format( Locale.ROOT, GAV_FORMAT, d.getGroupId(), d.getArtifactId(), d.getVersion() );
}
Expand All @@ -181,16 +208,48 @@ private static boolean shouldSkip(String gav, Set<Pattern> skipPatterns) {
}

private <T> T fetch(Gson gson, String url, Class<T> klass, Supplier<T> empty) throws EnforcerRuleException {
return withRetry(
() -> {
HttpRequest request = HttpRequest.newBuilder()
.uri( URI.create( url ) )
.header( "Content-Type", "application/json" )
.GET()
.build();

getLog().info( "Fetching from " + url );
HttpResponse<String> response = client.send( request, HttpResponse.BodyHandlers.ofString() );

return gson.fromJson( response.body(), klass );
}, empty
);
}

private <T> T post(Gson gson, HttpClient client, String url, CentralSearchRequest searchRequest, Class<T> klass,
Supplier<T> empty)
throws EnforcerRuleException {
return withRetry(
() -> {
HttpRequest request = HttpRequest.newBuilder()
.uri( URI.create( url ) )
.header( "Content-Type", "application/json" )
.POST( HttpRequest.BodyPublishers.ofString( gson.toJson( searchRequest ) ) )
.build();

HttpResponse<String> response = client.send( request, HttpResponse.BodyHandlers.ofString() );

return gson.fromJson( response.body(), klass );
}, empty
);
}

private <T> T withRetry(ThrowingSupplier<T> action, Supplier<T> empty) throws EnforcerRuleException {
for ( int i = 0; i < MAX_RETRIES; i++ ) {
try (
var stream = URI.create( url ).toURL().openStream(); var isr = new InputStreamReader( stream );
var reader = new JsonReader( isr )
) {
getLog().info( "Fetching from " + url );
return gson.fromJson( reader, klass );
try {
return action.get();

}
catch (IOException e) {
getLog().warn( "Fetching from " + url + " failed. Retrying in " + RETRY_DELAY_SECONDS
getLog().warn( "Fetching failed. Retrying in " + RETRY_DELAY_SECONDS
+ "s... (Attempt " + ( i + 1 ) + "/" + ( MAX_RETRIES ) + "): " + e.getMessage() );
try {
TimeUnit.SECONDS.sleep( RETRY_DELAY_SECONDS );
Expand All @@ -200,8 +259,12 @@ private <T> T fetch(Gson gson, String url, Class<T> klass, Supplier<T> empty) th
throw new EnforcerRuleException( ie );
}
}
catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new EnforcerRuleException( ie );
}
}
getLog().warn( "Fetching from " + url + " failed after " + ( MAX_RETRIES ) + " attempts." );
getLog().warn( "Fetching from failed after " + ( MAX_RETRIES ) + " attempts." );
return empty.get();
}

Expand All @@ -223,6 +286,12 @@ public Artifact(String g, String a, String v) {
public Artifact() {
}

public Artifact(CentralComponent centralComponent) {
this.g = centralComponent.namespace;
this.a = centralComponent.name;
this.v = centralComponent.version;
}

public String formattedString() {
return String.format( Locale.ROOT, GAV_FORMAT, g, a, v );
}
Expand Down Expand Up @@ -320,4 +389,65 @@ static StatusSummary empty() {
return statusSummary;
}
}

// Central search API DTOs:
static class CentralSearchRequest {
public int page;
public int size;
public String searchTerm;
public List<Object> filter;

CentralSearchRequest(Artifact filter) {
this.page = 0;
this.size = 20;
this.filter = null;
StringBuilder queryBuilder = new StringBuilder();
queryBuilder.append( "namespace:" ).append( filter.g );

if ( filter.a != null && !filter.a.trim().isEmpty() ) {
queryBuilder.append( " name:" ).append( filter.a );
}
if ( filter.v != null && !filter.v.trim().isEmpty() ) {
queryBuilder.append( " version:" ).append( filter.v );
}

this.searchTerm = queryBuilder.toString();
}

int nextPage() {
return this.page++;
}

@Override
public String toString() {
return searchTerm;
}
}

static class CentralSearchResponse {
public List<CentralComponent> components;
public int page;
public int pageSize;
public int pageCount;
public int totalResultCount;
public int totalCount;

static CentralSearchResponse empty() {
CentralSearchResponse res = new CentralSearchResponse();
res.components = List.of();
return res;
}
}

static class CentralComponent {
public String id;
public String namespace;
public String name;
public String version;
}

private interface ThrowingSupplier<T> {

T get() throws IOException, InterruptedException;
}
}
Loading