12
12
import java.io.InputStreamReader;
13
13
import java.net.URI;
14
14
import java.net.URLEncoder;
15
+ import java.net.http.HttpClient;
16
+ import java.net.http.HttpRequest;
17
+ import java.net.http.HttpResponse;
15
18
import java.nio.charset.StandardCharsets;
16
19
import java.util.ArrayList;
17
20
import java.util.HashSet;
@@ -45,6 +48,7 @@ public class DependencyManagementIncludesAllGroupIdArtifactsRule extends Abstrac
45
48
*/
46
49
private static final String BASE_URL_FORMAT =
47
50
"https://search.maven.org/solrsearch/select?q=%s&core=gav&start=%d&rows=%d&wt=json";
51
+ private static final String CENTRAL_SEARCH_URL = "https://central.sonatype.com/api/internal/browse/components";
48
52
private static final String MAVEN_STATUS_URL = "https://status.maven.org/api/v2/summary.json";
49
53
private static final int ROWS_PER_PAGE = 100;
50
54
private static final int MAX_RETRIES = 5;
@@ -71,6 +75,8 @@ public class DependencyManagementIncludesAllGroupIdArtifactsRule extends Abstrac
71
75
*/
72
76
private Set<Dependency> dependenciesToSkip = new HashSet<>();
73
77
78
+ private HttpClient client = HttpClient.newBuilder().build();
79
+
74
80
public void execute() throws EnforcerRuleException {
75
81
Set<String> dependencies = session.getCurrentProject()
76
82
.getDependencyManagement()
@@ -123,27 +129,7 @@ public void execute() throws EnforcerRuleException {
123
129
124
130
if ( mavenStatus.isSearchAvailable() ) {
125
131
for ( Artifact filter : toQuery ) {
126
- StringBuilder queryBuilder = new StringBuilder();
127
- queryBuilder.append( "g:" ).append( encodeValue( filter.g ) );
128
-
129
- if ( filter.a != null && !filter.a.trim().isEmpty() ) {
130
- queryBuilder.append( "+AND+a:" ).append( encodeValue( filter.a ) );
131
- }
132
- if ( filter.v != null && !filter.v.trim().isEmpty() ) {
133
- queryBuilder.append( "+AND+v:" ).append( encodeValue( filter.v ) );
134
- }
135
-
136
- int start = 0;
137
- do {
138
- String url = String.format( Locale.ROOT, BASE_URL_FORMAT, queryBuilder, start, ROWS_PER_PAGE );
139
- SearchResponse response = fetch( gson, url, SearchResponse.class, SearchResponse::empty );
140
- foundArtifacts.addAll( response.response.docs );
141
- if ( response.response.start + response.response.docs.size() >= response.response.numFound ) {
142
- break;
143
- }
144
- start += ROWS_PER_PAGE;
145
- }
146
- while ( true );
132
+ mavenCentralSearch( filter, gson, foundArtifacts );
147
133
}
148
134
}
149
135
else {
@@ -167,6 +153,45 @@ public void execute() throws EnforcerRuleException {
167
153
}
168
154
}
169
155
156
+ private void mavenCentralSearch(Artifact filter, Gson gson, Set<Artifact> foundArtifacts) throws EnforcerRuleException {
157
+ CentralSearchRequest request = new CentralSearchRequest( filter );
158
+ getLog().info( "Fetching information for " + request );
159
+ do {
160
+ CentralSearchResponse response =
161
+ post( gson, client, CENTRAL_SEARCH_URL, request, CentralSearchResponse.class,
162
+ CentralSearchResponse::empty );
163
+ foundArtifacts.addAll( response.components.stream().map( Artifact::new ).toList() );
164
+ if ( request.nextPage() >= response.pageCount ) {
165
+ break;
166
+ }
167
+ }
168
+ while ( true );
169
+ }
170
+
171
+ private void legacyMavenSolrSearch(Artifact filter, Gson gson, Set<Artifact> foundArtifacts) throws EnforcerRuleException {
172
+ StringBuilder queryBuilder = new StringBuilder();
173
+ queryBuilder.append( "g:" ).append( encodeValue( filter.g ) );
174
+
175
+ if ( filter.a != null && !filter.a.trim().isEmpty() ) {
176
+ queryBuilder.append( "+AND+a:" ).append( encodeValue( filter.a ) );
177
+ }
178
+ if ( filter.v != null && !filter.v.trim().isEmpty() ) {
179
+ queryBuilder.append( "+AND+v:" ).append( encodeValue( filter.v ) );
180
+ }
181
+
182
+ int start = 0;
183
+ do {
184
+ String url = String.format( Locale.ROOT, BASE_URL_FORMAT, queryBuilder, start, ROWS_PER_PAGE );
185
+ SearchResponse response = fetch( gson, url, SearchResponse.class, SearchResponse::empty );
186
+ foundArtifacts.addAll( response.response.docs );
187
+ if ( response.response.start + response.response.docs.size() >= response.response.numFound ) {
188
+ break;
189
+ }
190
+ start += ROWS_PER_PAGE;
191
+ }
192
+ while ( true );
193
+ }
194
+
170
195
private static String dependencyString(Dependency d) {
171
196
return String.format( Locale.ROOT, GAV_FORMAT, d.getGroupId(), d.getArtifactId(), d.getVersion() );
172
197
}
@@ -181,16 +206,45 @@ private static boolean shouldSkip(String gav, Set<Pattern> skipPatterns) {
181
206
}
182
207
183
208
private <T> T fetch(Gson gson, String url, Class<T> klass, Supplier<T> empty) throws EnforcerRuleException {
209
+ return withRetry(
210
+ () -> {
211
+ try (
212
+ var stream = URI.create( url ).toURL().openStream(); var isr = new InputStreamReader( stream );
213
+ var reader = new JsonReader( isr )
214
+ ) {
215
+ getLog().info( "Fetching from " + url );
216
+ return gson.fromJson( reader, klass );
217
+ }
218
+ }, empty
219
+ );
220
+ }
221
+
222
+ private <T> T post(Gson gson, HttpClient client, String url, CentralSearchRequest searchRequest, Class<T> klass,
223
+ Supplier<T> empty)
224
+ throws EnforcerRuleException {
225
+ return withRetry(
226
+ () -> {
227
+ HttpRequest request = HttpRequest.newBuilder()
228
+ .uri( URI.create( url ) )
229
+ .header( "Content-Type", "application/json" )
230
+ .POST( HttpRequest.BodyPublishers.ofString( gson.toJson( searchRequest ) ) )
231
+ .build();
232
+
233
+ HttpResponse<String> response = client.send( request, HttpResponse.BodyHandlers.ofString() );
234
+
235
+ return gson.fromJson( response.body(), klass );
236
+ }, empty
237
+ );
238
+ }
239
+
240
+ private <T> T withRetry(ThrowingSupplier<T> action, Supplier<T> empty) throws EnforcerRuleException {
184
241
for ( int i = 0; i < MAX_RETRIES; i++ ) {
185
- try (
186
- var stream = URI.create( url ).toURL().openStream(); var isr = new InputStreamReader( stream );
187
- var reader = new JsonReader( isr )
188
- ) {
189
- getLog().info( "Fetching from " + url );
190
- return gson.fromJson( reader, klass );
242
+ try {
243
+ return action.get();
244
+
191
245
}
192
246
catch (IOException e) {
193
- getLog().warn( "Fetching from " + url + " failed. Retrying in " + RETRY_DELAY_SECONDS
247
+ getLog().warn( "Fetching failed. Retrying in " + RETRY_DELAY_SECONDS
194
248
+ "s... (Attempt " + ( i + 1 ) + "/" + ( MAX_RETRIES ) + "): " + e.getMessage() );
195
249
try {
196
250
TimeUnit.SECONDS.sleep( RETRY_DELAY_SECONDS );
@@ -200,8 +254,12 @@ private <T> T fetch(Gson gson, String url, Class<T> klass, Supplier<T> empty) th
200
254
throw new EnforcerRuleException( ie );
201
255
}
202
256
}
257
+ catch (InterruptedException ie) {
258
+ Thread.currentThread().interrupt();
259
+ throw new EnforcerRuleException( ie );
260
+ }
203
261
}
204
- getLog().warn( "Fetching from " + url + " failed after " + ( MAX_RETRIES ) + " attempts." );
262
+ getLog().warn( "Fetching from failed after " + ( MAX_RETRIES ) + " attempts." );
205
263
return empty.get();
206
264
}
207
265
@@ -223,6 +281,12 @@ public Artifact(String g, String a, String v) {
223
281
public Artifact() {
224
282
}
225
283
284
+ public Artifact(CentralComponent centralComponent) {
285
+ this.g = centralComponent.namespace;
286
+ this.a = centralComponent.name;
287
+ this.v = centralComponent.version;
288
+ }
289
+
226
290
public String formattedString() {
227
291
return String.format( Locale.ROOT, GAV_FORMAT, g, a, v );
228
292
}
@@ -320,4 +384,65 @@ static StatusSummary empty() {
320
384
return statusSummary;
321
385
}
322
386
}
387
+
388
+ // Central search API DTOs:
389
+ static class CentralSearchRequest {
390
+ public int page;
391
+ public int size;
392
+ public String searchTerm;
393
+ public List<Object> filter;
394
+
395
+ CentralSearchRequest(Artifact filter) {
396
+ this.page = 0;
397
+ this.size = 20;
398
+ this.filter = null;
399
+ StringBuilder queryBuilder = new StringBuilder();
400
+ queryBuilder.append( "namespace:" ).append( filter.g );
401
+
402
+ if ( filter.a != null && !filter.a.trim().isEmpty() ) {
403
+ queryBuilder.append( " name:" ).append( filter.a );
404
+ }
405
+ if ( filter.v != null && !filter.v.trim().isEmpty() ) {
406
+ queryBuilder.append( " version:" ).append( filter.v );
407
+ }
408
+
409
+ this.searchTerm = queryBuilder.toString();
410
+ }
411
+
412
+ int nextPage() {
413
+ return this.page++;
414
+ }
415
+
416
+ @Override
417
+ public String toString() {
418
+ return searchTerm;
419
+ }
420
+ }
421
+
422
+ static class CentralSearchResponse {
423
+ public List<CentralComponent> components;
424
+ public int page;
425
+ public int pageSize;
426
+ public int pageCount;
427
+ public int totalResultCount;
428
+ public int totalCount;
429
+
430
+ static CentralSearchResponse empty() {
431
+ CentralSearchResponse res = new CentralSearchResponse();
432
+ res.components = List.of();
433
+ return res;
434
+ }
435
+ }
436
+
437
+ static class CentralComponent {
438
+ public String id;
439
+ public String namespace;
440
+ public String name;
441
+ public String version;
442
+ }
443
+
444
+ private interface ThrowingSupplier<T> {
445
+
446
+ T get() throws IOException, InterruptedException;
447
+ }
323
448
}
0 commit comments