Skip to content
Open
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
79 changes: 60 additions & 19 deletions src/main/java/com/giovds/QueryClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,29 @@
import org.apache.maven.plugin.MojoExecutionException;

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;
import java.time.Duration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executors;

/**
* Search Maven Central with a Lucene query
*/
public class QueryClient {
private final String main_uri;
private static final HttpClient client = HttpClient.newHttpClient();
private static final HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.connectTimeout(Duration.ofSeconds(50))
.executor(Executors.newFixedThreadPool(8))
.build();
private static final int MAX_RETRIES = 5;
private static final Duration INITIAL_RETRY_DELAY = Duration.ofSeconds(1);
Comment on lines +25 to +31

/**
* Visible for testing purposes
Expand All @@ -45,45 +52,79 @@ public QueryClient() {
* @throws MojoExecutionException when something failed when sending the request
*/
public Set<DependencyResponse> search(final List<String> queries) throws MojoExecutionException {
try {
final Set<DependencyResponse> allDependencies = new HashSet<>();
for (final String query : queries) {
final Set<DependencyResponse> allDependencies = new HashSet<>();
for (final String query : queries) {
allDependencies.addAll(searchWithRetry(query));
}
return allDependencies;
}

private Set<DependencyResponse> searchWithRetry(final String query) throws MojoExecutionException {
Exception lastException = null;

for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
final HttpRequest request = buildHttpRequest(query);
allDependencies.addAll(client.send(request, new SearchResponseBodyHandler()).body());
return client.send(request, new SearchResponseBodyHandler()).body();
} catch (IOException | InterruptedException e) {
lastException = e;

if (Thread.interrupted()) {
Thread.currentThread().interrupt();
throw new MojoExecutionException("Query interrupted", e);
}
Comment on lines +69 to +75

// Don't retry on last attempt
if (attempt < MAX_RETRIES - 1) {
final long delayMillis = INITIAL_RETRY_DELAY.toMillis() * (long) Math.pow(2, attempt);
try {
Thread.sleep(delayMillis);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new MojoExecutionException("Query interrupted during retry", ie);
}
}
}
return allDependencies;
} catch (Exception e) {
throw new MojoExecutionException("Failed to connect.", e);
}

throw new MojoExecutionException("Failed to query Maven Central after " + MAX_RETRIES + " attempts", lastException);
Comment on lines +65 to +90
}

private HttpRequest buildHttpRequest(final String query) {
final String uri = String.format("%s?q=%s&wt=json", main_uri, query);
return HttpRequest.newBuilder()
.GET()
.version(HttpClient.Version.HTTP_2)
.uri(URI.create(uri))
.timeout(Duration.ofSeconds(60))
.header("Accept", "application/json")
.header("User-Agent", "outdated-maven-plugin/1.5.0")
.build();
}

private static class SearchResponseBodyHandler implements HttpResponse.BodyHandler<Set<DependencyResponse>> {
@Override
public HttpResponse.BodySubscriber<Set<DependencyResponse>> apply(final HttpResponse.ResponseInfo responseInfo) {
if (responseInfo.statusCode() > 201) {
return HttpResponse.BodySubscribers.mapping(HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8), s -> {
throw new RuntimeException("Search failed: status: " + responseInfo.statusCode() + " body: " + s);
});
return HttpResponse.BodySubscribers.mapping(
HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8),
s -> {
throw new RuntimeException("Search failed: status: " + responseInfo.statusCode() + " body: " + s);
}
);
}
HttpResponse.BodySubscriber<InputStream> stream = HttpResponse.BodySubscribers.ofInputStream();
return HttpResponse.BodySubscribers.mapping(stream, this::toSearchResponse);
// Buffer entire response before processing - avoids stream lifecycle issues
return HttpResponse.BodySubscribers.mapping(
HttpResponse.BodySubscribers.ofByteArray(),
this::toSearchResponse
);
}

Set<DependencyResponse> toSearchResponse(final InputStream inputStream) {
try (final InputStream input = inputStream) {
final MavenCentralResponse mavenCentralResponse = JSON.std.beanFrom(MavenCentralResponse.class, input);
Set<DependencyResponse> toSearchResponse(final byte[] responseBytes) {
try {
final MavenCentralResponse mavenCentralResponse = JSON.std.beanFrom(MavenCentralResponse.class, responseBytes);
return new HashSet<>(mavenCentralResponse.response().docs());
} catch (IOException e) {
throw new RuntimeException(e);
throw new RuntimeException("Failed to parse Maven Central response", e);
}
}

Expand Down
Loading