diff --git a/src/main/java/com/giovds/QueryClient.java b/src/main/java/com/giovds/QueryClient.java index 40f7565..e545d33 100644 --- a/src/main/java/com/giovds/QueryClient.java +++ b/src/main/java/com/giovds/QueryClient.java @@ -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); /** * Visible for testing purposes @@ -45,24 +52,52 @@ public QueryClient() { * @throws MojoExecutionException when something failed when sending the request */ public Set search(final List queries) throws MojoExecutionException { - try { - final Set allDependencies = new HashSet<>(); - for (final String query : queries) { + final Set allDependencies = new HashSet<>(); + for (final String query : queries) { + allDependencies.addAll(searchWithRetry(query)); + } + return allDependencies; + } + + private Set 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); + } + + // 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); } 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(); } @@ -70,20 +105,26 @@ private static class SearchResponseBodyHandler implements HttpResponse.BodyHandl @Override public HttpResponse.BodySubscriber> 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 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 toSearchResponse(final InputStream inputStream) { - try (final InputStream input = inputStream) { - final MavenCentralResponse mavenCentralResponse = JSON.std.beanFrom(MavenCentralResponse.class, input); + Set 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); } }