From 530adf804407b53d70e01ffded2e796e2554c8d3 Mon Sep 17 00:00:00 2001 From: Jonathan Schneider Date: Thu, 9 Jul 2026 12:57:54 -0400 Subject: [PATCH] Add RepositoryLatencyDiagnostic recipe for artifact repository latency Diagnoses slow or flaky artifact-repository connectivity by measuring how long it takes to fetch `maven-metadata.xml` from every effective repository known to a build. Sibling of DependencyResolutionDiagnostic: one row per repository in the new `Repository response latency` data table. Each repository is probed N times (default 10) through the effective HttpSender, so in a Moderne deployment the requests traverse the same worker -> gateway -> connector -> repository path a real recipe run uses. The table records each request's HTTP status and the latency deciles, plus derived columns that attribute latency to a layer as far as the available signals allow: - a cache-busting companion probe (unique query string) measures true repository latency even when a connector cache is in play; - a cold/warm split isolates the connector cache hit (Moderne-internal round trip) from the repository fetch; - any `Server-Timing` response header is parsed into repo/rsocket columns; - a heuristic `probableBottleneck` classifies infra vs. repo vs. auth vs. intermittent causes. Raw HttpSender.send() is used rather than MavenPomDownloader.downloadMetadata so that repeated requests actually hit the network (the downloader caches metadata with no TTL). --- .../RepositoryLatencyDiagnostic.java | 453 ++++++++++++++++++ .../table/RepositoryResponseLatency.java | 183 +++++++ .../resources/META-INF/rewrite/recipes.csv | 35 +- .../RepositoryLatencyDiagnosticTest.java | 207 ++++++++ 4 files changed, 862 insertions(+), 16 deletions(-) create mode 100644 src/main/java/org/openrewrite/java/dependencies/RepositoryLatencyDiagnostic.java create mode 100644 src/main/java/org/openrewrite/java/dependencies/table/RepositoryResponseLatency.java create mode 100644 src/test/java/org/openrewrite/java/dependencies/RepositoryLatencyDiagnosticTest.java diff --git a/src/main/java/org/openrewrite/java/dependencies/RepositoryLatencyDiagnostic.java b/src/main/java/org/openrewrite/java/dependencies/RepositoryLatencyDiagnostic.java new file mode 100644 index 00000000..a7b22906 --- /dev/null +++ b/src/main/java/org/openrewrite/java/dependencies/RepositoryLatencyDiagnostic.java @@ -0,0 +1,453 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed 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 + *

+ * https://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.openrewrite.java.dependencies; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.*; +import org.openrewrite.gradle.marker.GradleProject; +import org.openrewrite.ipc.http.HttpSender; +import org.openrewrite.java.dependencies.table.RepositoryResponseLatency; +import org.openrewrite.maven.MavenExecutionContextView; +import org.openrewrite.maven.MavenSettings; +import org.openrewrite.maven.internal.MavenPomDownloader; +import org.openrewrite.maven.tree.MavenRepository; +import org.openrewrite.maven.tree.MavenRepositoryMirror; +import org.openrewrite.maven.tree.MavenResolutionResult; + +import java.io.UncheckedIOException; +import java.time.Duration; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.Collections.emptyMap; +import static org.openrewrite.internal.StringUtils.isBlank; + +@EqualsAndHashCode(callSuper = false) +@Value +public class RepositoryLatencyDiagnostic extends ScanningRecipe { + + private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(10); + private static final Duration READ_TIMEOUT = Duration.ofSeconds(30); + private static final Pattern SERVER_TIMING_DUR = Pattern.compile("dur=([0-9.]+)"); + // Latency thresholds (ms) used only for the heuristic bottleneck classification. + private static final long HEALTHY_MS = 500; + private static final long SLOW_MS = 2000; + + transient RepositoryResponseLatency latencies = new RepositoryResponseLatency(this); + + String displayName = "Artifact repository latency diagnostic"; + + String description = "Measures how long it takes to fetch a Maven metadata file from every artifact repository " + + "known to the build, to help diagnose slow or flaky repository connectivity. For each effective " + + "repository it requests `maven-metadata.xml` for a configurable artifact several times and records " + + "the HTTP status of each request and the latency deciles in the `Repository response latency` data " + + "table. \n\n" + + "Requests are issued through the same network path a real recipe run uses, so in a Moderne " + + "deployment they traverse the worker, gateway and connector before reaching the repository. The " + + "recipe only observes end-to-end time, so it additionally fires cache-busting requests and reads " + + "any `Server-Timing` response header to attribute latency to the repository versus Moderne " + + "infrastructure as far as the available signals allow."; + + @Option(displayName = "Group ID", + description = "The group ID of the artifact whose `maven-metadata.xml` is fetched. " + + "Default value is \"com.fasterxml.jackson.core\".", + example = "com.fasterxml.jackson.core", + required = false) + @Nullable + String groupId; + + @Option(displayName = "Artifact ID", + description = "The artifact ID of the artifact whose `maven-metadata.xml` is fetched. " + + "Default value is \"jackson-core\".", + example = "jackson-core", + required = false) + @Nullable + String artifactId; + + @Option(displayName = "Requests per repository", + description = "How many times to request the metadata from each repository. Default value is 10. " + + "The data table always reports the first 10 request statuses and 10 latency deciles.", + example = "10", + required = false) + @Nullable + Integer requestsPerRepository; + + public static class Accumulator { + boolean foundGradle; + Set repositoriesFromGradle = new HashSet<>(); + + boolean foundMaven; + Set repositoriesFromMaven = new HashSet<>(); + } + + @Override + public Accumulator getInitialValue(ExecutionContext ctx) { + return new Accumulator(); + } + + @Override + public TreeVisitor getScanner(Accumulator acc) { + return new TreeVisitor() { + @Override + public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + if (!(tree instanceof SourceFile)) { + return null; + } + tree.getMarkers().findFirst(GradleProject.class).ifPresent(gp -> { + acc.foundGradle = true; + acc.repositoriesFromGradle.addAll(gp.getMavenRepositories()); + acc.repositoriesFromGradle.addAll(gp.getMavenPluginRepositories()); + }); + tree.getMarkers().findFirst(MavenResolutionResult.class).ifPresent(mrr -> { + acc.foundMaven = true; + acc.repositoriesFromMaven.addAll(mrr.getPom().getRepositories()); + }); + return tree; + } + }; + } + + @Override + public Collection generate(Accumulator acc, ExecutionContext ctx) { + Set seen = new HashSet<>(); + if (acc.foundMaven) { + probeAll(true, acc.repositoriesFromMaven, seen, ctx); + } + if (acc.foundGradle) { + probeAll(false, acc.repositoriesFromGradle, seen, ctx); + } + return Collections.emptyList(); + } + + private void probeAll(boolean addMavenCentral, Collection repos, Set seen, ExecutionContext ctx) { + Collection effectiveRepos = repos; + if (addMavenCentral && !effectiveRepos.contains(MavenRepository.MAVEN_CENTRAL)) { + effectiveRepos = new ArrayList<>(effectiveRepos); + effectiveRepos.add(MavenRepository.MAVEN_CENTRAL); + } + + HttpSender httpSender = HttpSenderExecutionContextView.view(ctx).getHttpSender(); + MavenExecutionContextView mctx = MavenExecutionContextView.view(ctx); + MavenPomDownloader mpd = new MavenPomDownloader(ctx); + + for (MavenRepository repo : effectiveRepos) { + MavenRepository target = mpd.normalizeRepository(repo, mctx, null); + String note = ""; + if (target == null) { + MavenSettings settings = mctx.getSettings(); + target = settings == null ? repo : MavenRepositoryMirror.apply(mctx.getMirrors(settings), repo); + note = "Repository root did not respond to a ping; probed anyway. "; + } + // Local repositories are on disk, not a network endpoint, so latency is not meaningful. + if (target.getUri().startsWith("file:")) { + continue; + } + if (seen.add(noTrailingSlash(target.getUri()))) { + latencies.insertRow(ctx, probeRepository(httpSender, target, note)); + } + } + } + + private RepositoryResponseLatency.Row probeRepository(HttpSender httpSender, MavenRepository repo, String note) { + String group = isBlank(groupId) ? "com.fasterxml.jackson.core" : groupId; + String artifact = isBlank(artifactId) ? "jackson-core" : artifactId; + int n = requestsPerRepository == null ? 10 : Math.max(1, requestsPerRepository); + + String base = repo.getUri().endsWith("/") ? repo.getUri() : repo.getUri() + "/"; + String metadataUrl = base + group.replace('.', '/') + "/" + artifact + "/maven-metadata.xml"; + + // One uncounted warm-up pays connection/proxy/certificate setup and primes any connector cache. + send(httpSender, repo, metadataUrl); + + List plain = new ArrayList<>(); + List statuses = new ArrayList<>(); + List respondedLatencies = new ArrayList<>(); + for (int i = 0; i < n; i++) { + Probe p = send(httpSender, repo, metadataUrl); + plain.add(p); + statuses.add(p.label); + if (p.responded) { + respondedLatencies.add(p.millis); + } + } + + // Cache-busting probes: a unique query string makes the connector cache miss and reach the repository every time. + long nonce = System.nanoTime(); + List cacheImmuneLatencies = new ArrayList<>(); + Probe immuneWithHeaders = null; + for (int i = 0; i < n; i++) { + String busted = metadataUrl + "?mrnLatencyProbe=" + nonce + "-" + i; + Probe p = send(httpSender, repo, busted); + if (p.responded) { + cacheImmuneLatencies.add(p.millis); + } + if (immuneWithHeaders == null && p.responded && !p.emptyHeaders) { + immuneWithHeaders = p; + } + } + + List sorted = new ArrayList<>(respondedLatencies); + Collections.sort(sorted); + + int responded = respondedLatencies.size(); + Long coldStartMs = plain.get(0).responded ? plain.get(0).millis : null; + List warm = new ArrayList<>(); + int warmSuccesses = 0; + int warmCacheHits = 0; + for (int i = 1; i < plain.size(); i++) { + Probe p = plain.get(i); + if (p.responded) { + warm.add(p.millis); + if (p.code != null && p.code < 400) { + warmSuccesses++; + if (!repoFetched(p)) { + warmCacheHits++; + } + } + } + } + Long warmMedianMs = median(warm); + Long cacheImmuneMedianMs = median(cacheImmuneLatencies); + Long estimatedRepoLatencyMs = warmMedianMs != null && cacheImmuneMedianMs != null ? + Math.max(0, cacheImmuneMedianMs - warmMedianMs) : null; + Boolean warmServedFromCache = warmSuccesses == 0 ? null : warmCacheHits == warmSuccesses; + + Long p10 = percentile(sorted, 10); + Long p90 = percentile(sorted, 90); + Long jitterMs = p10 != null && p90 != null ? p90 - p10 : null; + + Map> immuneHeaders = immuneWithHeaders == null ? emptyMap() : immuneWithHeaders.headers; + Long stRepo = serverTiming(immuneHeaders, "repo"); + Long stConnector = serverTiming(immuneHeaders, "connector"); + Long stRsocket = serverTiming(immuneHeaders, "rsocket"); + String upstreamServer = upstreamServer(immuneHeaders); + + String bottleneck = classify(statuses, responded, warmMedianMs, cacheImmuneMedianMs, + estimatedRepoLatencyMs, p10, p90, Boolean.TRUE.equals(warmServedFromCache), stRepo, stRsocket); + + return RepositoryResponseLatency.Row.builder() + .repositoryUri(noTrailingSlash(repo.getUri())) + .metadataUri(metadataUrl) + .groupArtifact(group + ":" + artifact) + .httpResponseCount(responded) + .connectionFailureCount(n - responded) + .status1(status(statuses, 0)) + .status2(status(statuses, 1)) + .status3(status(statuses, 2)) + .status4(status(statuses, 3)) + .status5(status(statuses, 4)) + .status6(status(statuses, 5)) + .status7(status(statuses, 6)) + .status8(status(statuses, 7)) + .status9(status(statuses, 8)) + .status10(status(statuses, 9)) + .latencyDecile1Ms(percentile(sorted, 10)) + .latencyDecile2Ms(percentile(sorted, 20)) + .latencyDecile3Ms(percentile(sorted, 30)) + .latencyDecile4Ms(percentile(sorted, 40)) + .latencyDecile5Ms(percentile(sorted, 50)) + .latencyDecile6Ms(percentile(sorted, 60)) + .latencyDecile7Ms(percentile(sorted, 70)) + .latencyDecile8Ms(percentile(sorted, 80)) + .latencyDecile9Ms(percentile(sorted, 90)) + .latencyDecile10Ms(percentile(sorted, 100)) + .coldStartMs(coldStartMs) + .warmMedianMs(warmMedianMs) + .cacheImmuneMedianMs(cacheImmuneMedianMs) + .estimatedRepoLatencyMs(estimatedRepoLatencyMs) + .jitterMs(jitterMs) + .warmServedFromConnectorCache(warmServedFromCache) + .serverTimingRepositoryMs(stRepo) + .serverTimingConnectorMs(stConnector) + .serverTimingRsocketMs(stRsocket) + .upstreamServer(upstreamServer) + .probableBottleneck(bottleneck) + .notes(note + distinctFailures(statuses)) + .build(); + } + + private static Probe send(HttpSender httpSender, MavenRepository repo, String url) { + HttpSender.Request request = httpSender.get(url) + .withBasicAuthentication(repo.getUsername(), repo.getPassword()) + .withConnectTimeout(CONNECT_TIMEOUT) + .withReadTimeout(READ_TIMEOUT) + .build(); + long start = System.nanoTime(); + try (HttpSender.Response response = httpSender.send(request)) { + int code = response.getCode(); + Map> headers = response.getHeaders(); + response.getBodyAsBytes(); // fully read the body so transfer time is included + long millis = (System.nanoTime() - start) / 1_000_000; + boolean empty = headers == null || headers.isEmpty(); + return new Probe(true, code, Integer.toString(code), millis, empty, headers == null ? emptyMap() : headers); + } catch (Throwable t) { + long millis = (System.nanoTime() - start) / 1_000_000; + Throwable cause = t; + while (cause instanceof UncheckedIOException && cause.getCause() != null) { + cause = cause.getCause(); + } + return new Probe(false, null, cause.getClass().getSimpleName(), millis, true, emptyMap()); + } + } + + private static String classify(List statuses, int responded, @Nullable Long warmMedian, + @Nullable Long cacheImmuneMedian, @Nullable Long estimatedRepo, + @Nullable Long p10, @Nullable Long p90, boolean warmCache, + @Nullable Long stRepo, @Nullable Long stRsocket) { + if (statuses.contains("503")) { + return "MODERNE_INFRA_NO_CONNECTOR"; + } + if (statuses.contains("502")) { + return "MODERNE_TUNNEL_ERROR"; + } + if (statuses.contains("401") || statuses.contains("403")) { + return "REPO_AUTH"; + } + if (statuses.contains("429")) { + return "REPO_RATE_LIMIT"; + } + if (responded == 0) { + return "UNREACHABLE"; + } + // When the tunnel reports a real per-hop breakdown, trust it over the heuristics. + if (stRepo != null || stRsocket != null) { + long repo = stRepo == null ? 0 : stRepo; + long rsocket = stRsocket == null ? 0 : stRsocket; + if (repo > rsocket && repo > SLOW_MS) { + return "REPO_FETCH_SLOW"; + } + if (rsocket > repo && rsocket > SLOW_MS) { + return "INTERNAL_TRANSPORT_SLOW"; + } + } + if (warmCache && estimatedRepo != null && estimatedRepo > SLOW_MS) { + return "REPO_FETCH_SLOW"; + } + if (warmMedian != null && warmMedian > SLOW_MS) { + return "INTERNAL_TRANSPORT_SLOW"; + } + boolean jittery = p10 != null && p90 != null && p90 > Math.max(HEALTHY_MS, p10 * 3); + if (jittery) { + return "INTERMITTENT"; + } + if (p90 != null && p90 > SLOW_MS) { + return cacheImmuneMedian != null && warmMedian != null && cacheImmuneMedian > warmMedian * 2 ? + "REPO_FETCH_SLOW" : "SLOW"; + } + return "HEALTHY"; + } + + private static String status(List statuses, int i) { + return i < statuses.size() ? statuses.get(i) : ""; + } + + private static String distinctFailures(List statuses) { + Set failures = new LinkedHashSet<>(); + for (String s : statuses) { + if (!s.isEmpty() && !s.chars().allMatch(Character::isDigit)) { + failures.add(s); + } + } + return failures.isEmpty() ? "" : "Transport failures: " + String.join(", ", failures); + } + + private static @Nullable Long median(List values) { + List sorted = new ArrayList<>(values); + Collections.sort(sorted); + return percentile(sorted, 50); + } + + private static @Nullable Long percentile(List sortedAsc, int p) { + if (sortedAsc.isEmpty()) { + return null; + } + int rank = (int) Math.ceil(p / 100.0 * sortedAsc.size()); + int idx = Math.min(Math.max(rank - 1, 0), sortedAsc.size() - 1); + return sortedAsc.get(idx); + } + + private static @Nullable Long serverTiming(Map> headers, String metric) { + String value = firstHeader(headers, "Server-Timing"); + if (value == null) { + return null; + } + for (String entry : value.split(",")) { + String trimmed = entry.trim(); + String name = trimmed.split(";", 2)[0].trim(); + if (name.equalsIgnoreCase(metric)) { + Matcher m = SERVER_TIMING_DUR.matcher(trimmed); + if (m.find()) { + return Math.round(Double.parseDouble(m.group(1))); + } + } + } + return null; + } + + private static String upstreamServer(Map> headers) { + List parts = new ArrayList<>(); + for (String name : new String[]{"Server", "Via", "X-Cache"}) { + String value = firstHeader(headers, name); + if (value != null) { + parts.add(name + ": " + value); + } + } + return String.join("; ", parts); + } + + /** + * Whether a probe actually reached the repository (versus being served from the connector cache). Robust across + * deployments: a {@code repo;dur} Server-Timing means the repo was fetched; a Server-Timing with no {@code repo;dur} + * (just {@code tunnel;dur}) means an internal-only round trip; otherwise fall back to header emptiness, since the + * connector returns empty headers for a cache hit but the repository's own headers for a real fetch. + */ + private static boolean repoFetched(Probe p) { + if (serverTiming(p.headers, "repo") != null) { + return true; + } + if (firstHeader(p.headers, "Server-Timing") != null) { + return false; + } + return !p.emptyHeaders; + } + + private static @Nullable String firstHeader(Map> headers, String name) { + for (Map.Entry> e : headers.entrySet()) { + if (name.equalsIgnoreCase(e.getKey()) && e.getValue() != null && !e.getValue().isEmpty()) { + return e.getValue().get(0); + } + } + return null; + } + + private static String noTrailingSlash(String uri) { + return uri.endsWith("/") ? uri.substring(0, uri.length() - 1) : uri; + } + + @Value + private static class Probe { + boolean responded; + @Nullable + Integer code; + String label; + long millis; + boolean emptyHeaders; + Map> headers; + } +} diff --git a/src/main/java/org/openrewrite/java/dependencies/table/RepositoryResponseLatency.java b/src/main/java/org/openrewrite/java/dependencies/table/RepositoryResponseLatency.java new file mode 100644 index 00000000..008e94c8 --- /dev/null +++ b/src/main/java/org/openrewrite/java/dependencies/table/RepositoryResponseLatency.java @@ -0,0 +1,183 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed 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 + *

+ * https://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.openrewrite.java.dependencies.table; + +import com.fasterxml.jackson.annotation.JsonIgnoreType; +import lombok.Builder; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.Column; +import org.openrewrite.DataTable; +import org.openrewrite.Recipe; + +@JsonIgnoreType +public class RepositoryResponseLatency extends DataTable { + + public RepositoryResponseLatency(Recipe recipe) { + super(recipe, + "Repository response latency", + "One row per effective artifact repository, recording the latency of repeatedly fetching a Maven " + + "metadata file. Because the recipe only observes end-to-end time, the derived columns attribute latency " + + "to a layer (Moderne infrastructure vs. the repository) as best they can from status codes, the connector " + + "cache hit/miss cliff, and any Server-Timing header returned by the tunnel."); + } + + @Value + @Builder + public static class Row { + @Column(displayName = "Repository URI", + description = "The effective (post-mirror, normalized) URI of the repository.") + String repositoryUri; + + @Column(displayName = "Maven metadata URI", + description = "The exact `maven-metadata.xml` URL that was probed.") + String metadataUri; + + @Column(displayName = "Group:artifact probed", + description = "The `groupId:artifactId` whose metadata was requested.") + String groupArtifact; + + @Column(displayName = "HTTP response count", + description = "How many of the probes returned any HTTP status (2xx-5xx). The remainder failed at the " + + "transport layer (timeout, unknown host, connection refused).") + Integer httpResponseCount; + + @Column(displayName = "Connection failure count", + description = "How many probes failed before receiving an HTTP status (timeout, unknown host, refused).") + Integer connectionFailureCount; + + @Column(displayName = "Request 1 status", description = "HTTP status, or the transport exception, of probe 1.") + String status1; + @Column(displayName = "Request 2 status", description = "HTTP status, or the transport exception, of probe 2.") + String status2; + @Column(displayName = "Request 3 status", description = "HTTP status, or the transport exception, of probe 3.") + String status3; + @Column(displayName = "Request 4 status", description = "HTTP status, or the transport exception, of probe 4.") + String status4; + @Column(displayName = "Request 5 status", description = "HTTP status, or the transport exception, of probe 5.") + String status5; + @Column(displayName = "Request 6 status", description = "HTTP status, or the transport exception, of probe 6.") + String status6; + @Column(displayName = "Request 7 status", description = "HTTP status, or the transport exception, of probe 7.") + String status7; + @Column(displayName = "Request 8 status", description = "HTTP status, or the transport exception, of probe 8.") + String status8; + @Column(displayName = "Request 9 status", description = "HTTP status, or the transport exception, of probe 9.") + String status9; + @Column(displayName = "Request 10 status", description = "HTTP status, or the transport exception, of probe 10.") + String status10; + + @Column(displayName = "Latency decile 1 (ms)", description = "10th percentile latency of the responding probes (fastest).") + @Nullable + Long latencyDecile1Ms; + @Column(displayName = "Latency decile 2 (ms)", description = "20th percentile latency of the responding probes.") + @Nullable + Long latencyDecile2Ms; + @Column(displayName = "Latency decile 3 (ms)", description = "30th percentile latency of the responding probes.") + @Nullable + Long latencyDecile3Ms; + @Column(displayName = "Latency decile 4 (ms)", description = "40th percentile latency of the responding probes.") + @Nullable + Long latencyDecile4Ms; + @Column(displayName = "Latency decile 5 (ms)", description = "50th percentile (median) latency of the responding probes.") + @Nullable + Long latencyDecile5Ms; + @Column(displayName = "Latency decile 6 (ms)", description = "60th percentile latency of the responding probes.") + @Nullable + Long latencyDecile6Ms; + @Column(displayName = "Latency decile 7 (ms)", description = "70th percentile latency of the responding probes.") + @Nullable + Long latencyDecile7Ms; + @Column(displayName = "Latency decile 8 (ms)", description = "80th percentile latency of the responding probes.") + @Nullable + Long latencyDecile8Ms; + @Column(displayName = "Latency decile 9 (ms)", description = "90th percentile latency of the responding probes.") + @Nullable + Long latencyDecile9Ms; + @Column(displayName = "Latency decile 10 (ms)", description = "100th percentile latency of the responding probes (slowest).") + @Nullable + Long latencyDecile10Ms; + + @Column(displayName = "Cold start latency (ms)", + description = "Latency of the first counted probe. Includes connection setup and, when the connector " + + "cache is cold, the full connector-to-repository fetch.") + @Nullable + Long coldStartMs; + + @Column(displayName = "Warm median latency (ms)", + description = "Median latency of probes 2..N. When the connector caches metadata these are cache hits, " + + "so this approximates the Moderne-internal round trip (worker->gateway->connector) with no " + + "repository fetch.") + @Nullable + Long warmMedianMs; + + @Column(displayName = "Cache-immune median latency (ms)", + description = "Median latency of the cache-busting probes (a unique query string forces the connector to " + + "reach the repository every time), i.e. the true full-path latency.") + @Nullable + Long cacheImmuneMedianMs; + + @Column(displayName = "Estimated connector->repository latency (ms)", + description = "Cache-immune median minus warm median. When the warm probes are connector cache hits, this " + + "isolates the connector-to-repository hop. Meaningless (near zero) when the connector is not " + + "caching — see the `Warm requests served from connector cache` column.") + @Nullable + Long estimatedRepoLatencyMs; + + @Column(displayName = "Latency jitter p90-p10 (ms)", + description = "Spread between the 90th and 10th percentile of the responding probes. High jitter points to " + + "intermittent causes (GC, RSocket head-of-line blocking, connector reconnects, CDN variance).") + @Nullable + Long jitterMs; + + @Column(displayName = "Warm requests served from connector cache", + description = "True when the warm probes returned quickly with empty response headers, the signature of a " + + "connector-side Maven cache hit. When false, every probe is a full-path request.") + @Nullable + Boolean warmServedFromConnectorCache; + + @Column(displayName = "Server-Timing repository (ms)", + description = "Connector-measured time spent fetching from the repository, read from the `Server-Timing` " + + "response header. Empty until the gateway/connector emit it.") + @Nullable + Long serverTimingRepositoryMs; + + @Column(displayName = "Server-Timing connector (ms)", + description = "Connector-measured in-connector processing time, read from the `Server-Timing` response header. " + + "Empty until the gateway/connector emit it.") + @Nullable + Long serverTimingConnectorMs; + + @Column(displayName = "Server-Timing RSocket (ms)", + description = "Gateway-measured gateway<->connector RSocket exchange time, read from the `Server-Timing` " + + "response header. Empty until the gateway/connector emit it.") + @Nullable + Long serverTimingRsocketMs; + + @Column(displayName = "Upstream server header", + description = "The `Server`/`Via`/`X-Cache` response headers of a cache-immune probe, revealing a CDN or " + + "proxy in front of the repository. Empty for connector cache hits (which strip headers).") + String upstreamServer; + + @Column(displayName = "Probable bottleneck", + description = "A heuristic classification of where the latency or failure most likely originates.") + String probableBottleneck; + + @Column(displayName = "Notes", + description = "Free-form detail: transport exceptions, whether a mirror was applied, unreachable reasons.") + String notes; + } +} diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index 82d291e6..4e143f34 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -1,30 +1,32 @@ ecosystem,packageName,name,displayName,description,recipeCount,category1,category2,category3,category1Description,category2Description,category3Description,options,dataTables -maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.UpgradeTransitiveDependencyVersion,Upgrade transitive Gradle or Maven dependencies,"Upgrades the version of a transitive dependency in a Maven pom.xml or Gradle build.gradle. Leaves direct dependencies unmodified. Can be paired with the regular Upgrade Dependency Version recipe to upgrade a dependency everywhere, regardless of whether it is direct or transitive.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupId"",""type"":""String"",""displayName"":""Group"",""description"":""The first part of a dependency coordinate 'org.apache.logging.log4j:ARTIFACT_ID:VERSION'."",""example"":""org.apache.logging.log4j"",""required"":true},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact"",""description"":""The second part of a dependency coordinate 'org.apache.logging.log4j:log4j-bom:VERSION'."",""example"":""log4j-bom"",""required"":true},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""An exact version number or node-style semver selector used to select the version number."",""example"":""latest.release"",""required"":true},{""name"":""scope"",""type"":""String"",""displayName"":""Scope"",""description"":""An optional scope to use for the dependency management tag. Relevant only to Maven."",""example"":""import"",""valid"":[""import"",""runtime"",""provided"",""test""]},{""name"":""type"",""type"":""String"",""displayName"":""Type"",""description"":""An optional type to use for the dependency management tag. Relevant only to Maven builds."",""example"":""pom"",""valid"":[""jar"",""pom"",""war""]},{""name"":""classifier"",""type"":""String"",""displayName"":""Classifier"",""description"":""An optional classifier to use for the dependency management tag. Relevant only to Maven."",""example"":""test""},{""name"":""versionPattern"",""type"":""String"",""displayName"":""Version pattern"",""description"":""Allows version selection to be extended beyond the original Node Semver semantics. So for example,Setting 'version' to \""25-29\"" can be paired with a metadata pattern of \""-jre\"" to select 29.0-jre"",""example"":""-jre""},{""name"":""because"",""type"":""String"",""displayName"":""Because"",""description"":""The reason for upgrading the transitive dependency. For example, we could be responding to a vulnerability."",""example"":""CVE-2021-1234""},{""name"":""releasesOnly"",""type"":""Boolean"",""displayName"":""Releases only"",""description"":""Whether to exclude snapshots from consideration when using a semver selector""},{""name"":""onlyIfUsing"",""type"":""String"",""displayName"":""Only if using glob expression for group:artifact"",""description"":""Only add managed dependencies to projects having a dependency matching the expression."",""example"":""org.apache.logging.log4j:log4j*""},{""name"":""addToRootPom"",""type"":""Boolean"",""displayName"":""Add to the root pom"",""description"":""Add to the root pom where root is the eldest parent of the pom within the source set.""}]", +maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.AddDependency,Add Gradle or Maven dependency,"For a Gradle project, add a gradle dependency to a `build.gradle` file in the correct configuration based on where it is used. Or For a maven project, Add a Maven dependency to a `pom.xml` file in the correct scope based on where it is used.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupId"",""type"":""String"",""displayName"":""Group ID"",""description"":""The first part of a dependency coordinate `com.google.guava:guava:VERSION`."",""example"":""com.google.guava"",""required"":true},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact ID"",""description"":""The second part of a dependency coordinate `com.google.guava:guava:VERSION`"",""example"":""guava"",""required"":true},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""An exact version number or node-style semver selector used to select the version number."",""example"":""29.X""},{""name"":""versionPattern"",""type"":""String"",""displayName"":""Version pattern"",""description"":""Allows version selection to be extended beyond the original Node Semver semantics. So for example, Setting 'version' to \""25-29\"" can be paired with a metadata pattern of \""-jre\"" to select Guava 29.0-jre"",""example"":""-jre""},{""name"":""onlyIfUsing"",""type"":""String"",""displayName"":""Only if using"",""description"":""Used to determine if the dependency will be added and in which scope it should be placed."",""example"":""org.junit.jupiter.api.*""},{""name"":""classifier"",""type"":""String"",""displayName"":""Classifier"",""description"":""A classifier to add. Commonly used to select variants of a library."",""example"":""test""},{""name"":""familyPattern"",""type"":""String"",""displayName"":""Family pattern"",""description"":""A pattern, applied to groupIds, used to determine which other dependencies should have aligned version numbers. Accepts '*' as a wildcard character."",""example"":""com.fasterxml.jackson*""},{""name"":""extension"",""type"":""String"",""displayName"":""Extension"",""description"":""For Gradle only, The extension of the dependency to add. If omitted Gradle defaults to assuming the type is \""jar\""."",""example"":""jar""},{""name"":""configuration"",""type"":""String"",""displayName"":""Gradle configuration"",""description"":""The Gradle dependency configuration name within which to place the dependency. When omitted the configuration will be determined by the Maven scope parameter. If that parameter is also omitted, configuration will be determined based on where types matching `onlyIfUsing` appear in source code."",""example"":""implementation""},{""name"":""scope"",""type"":""String"",""displayName"":""Maven scope"",""description"":""The Maven scope within which to place the dependency. When omitted scope will be determined based on where types matching `onlyIfUsing` appear in source code."",""example"":""runtime"",""valid"":[""compile"",""provided"",""runtime"",""test""]},{""name"":""releasesOnly"",""type"":""Boolean"",""displayName"":""Releases only"",""description"":""For Maven only, Whether to exclude snapshots from consideration when using a semver selector""},{""name"":""type"",""type"":""String"",""displayName"":""Type"",""description"":""For Maven only, The type of dependency to add. If omitted Maven defaults to assuming the type is \""jar\""."",""example"":""jar"",""valid"":[""jar"",""pom"",""war""]},{""name"":""optional"",""type"":""Boolean"",""displayName"":""Optional"",""description"":""Set the value of the `` tag. No `` tag will be added when this is `null`.""},{""name"":""acceptTransitive"",""type"":""Boolean"",""displayName"":""Accept transitive"",""description"":""Default false. If enabled, the dependency will not be added if it is already on the classpath as a transitive dependency."",""example"":""true""}]", maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.ChangeDependency,Change Gradle or Maven dependency,"Change the group ID, artifact ID, and/or the version of a specified Gradle or Maven dependency.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""oldGroupId"",""type"":""String"",""displayName"":""Old group ID"",""description"":""The old group ID to replace. The group ID is the first part of a dependency coordinate 'com.google.guava:guava:VERSION'. Supports glob expressions."",""example"":""org.openrewrite.recipe"",""required"":true},{""name"":""oldArtifactId"",""type"":""String"",""displayName"":""Old artifact ID"",""description"":""The old artifact ID to replace. The artifact ID is the second part of a dependency coordinate 'com.google.guava:guava:VERSION'. Supports glob expressions."",""example"":""rewrite-testing-frameworks"",""required"":true},{""name"":""newGroupId"",""type"":""String"",""displayName"":""New group ID"",""description"":""The new group ID to use. Defaults to the existing group ID."",""example"":""corp.internal.openrewrite.recipe""},{""name"":""newArtifactId"",""type"":""String"",""displayName"":""New artifact ID"",""description"":""The new artifact ID to use. Defaults to the existing artifact ID."",""example"":""rewrite-testing-frameworks""},{""name"":""newVersion"",""type"":""String"",""displayName"":""New version"",""description"":""An exact version number or node-style semver selector used to select the version number."",""example"":""29.X""},{""name"":""versionPattern"",""type"":""String"",""displayName"":""Version pattern"",""description"":""Allows version selection to be extended beyond the original Node Semver semantics. So for example,Setting 'version' to \""25-29\"" can be paired with a metadata pattern of \""-jre\"" to select Guava 29.0-jre"",""example"":""-jre""},{""name"":""overrideManagedVersion"",""type"":""Boolean"",""displayName"":""Override managed version"",""description"":""If the new dependency has a managed version, this flag can be used to explicitly set the version on the dependency. The default for this flag is `false`.""},{""name"":""changeManagedDependency"",""type"":""Boolean"",""displayName"":""Update dependency management"",""description"":""Also update the dependency management section. The default for this flag is `true`.""}]", +maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.DependencyInsight,Dependency insight for Gradle and Maven,"Finds dependencies, including transitive dependencies, in both Gradle and Maven projects. Matches within all Gradle dependency configurations and Maven scopes.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupIdPattern"",""type"":""String"",""displayName"":""Group pattern"",""description"":""Group ID glob pattern used to match dependencies."",""example"":""com.fasterxml.jackson*"",""required"":true},{""name"":""artifactIdPattern"",""type"":""String"",""displayName"":""Artifact pattern"",""description"":""Artifact ID glob pattern used to match dependencies."",""example"":""jackson-*"",""required"":true},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""Match only dependencies with the specified version. Node-style [version selectors](https://docs.openrewrite.org/reference/dependency-version-selectors) may be used. All versions are searched by default."",""example"":""1.x""},{""name"":""scope"",""type"":""String"",""displayName"":""Scope"",""description"":""Match dependencies with the specified Maven scope. All scopes are searched by default."",""example"":""compile"",""valid"":[""compile"",""test"",""runtime"",""provided"",""system""]}]","[{""name"":""org.openrewrite.maven.table.DependenciesInUse"",""displayName"":""Dependencies in use"",""instanceName"":""Dependencies in use"",""description"":""Direct and transitive dependencies in use."",""columns"":[{""name"":""projectName"",""type"":""String"",""displayName"":""Project name"",""description"":""The name of the project that contains the dependency.""},{""name"":""sourceSet"",""type"":""String"",""displayName"":""Source set"",""description"":""The source set that contains the dependency.""},{""name"":""groupId"",""type"":""String"",""displayName"":""Group"",""description"":""The first part of a dependency coordinate `com.google.guava:guava:VERSION`.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact"",""description"":""The second part of a dependency coordinate `com.google.guava:guava:VERSION`.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The resolved version.""},{""name"":""datedSnapshotVersion"",""type"":""String"",""displayName"":""Dated snapshot version"",""description"":""The resolved dated snapshot version or `null` if this dependency is not a snapshot.""},{""name"":""scope"",""type"":""String"",""displayName"":""Scope"",""description"":""Dependency scope. This will be `compile` if the dependency is direct and a scope is not explicitly specified in the POM.""},{""name"":""count"",""type"":""Integer"",""displayName"":""Count"",""description"":""How many times does this dependency appear.""}]}]" +maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.DependencyList,Dependency report,Emits a data table detailing all Gradle and Maven dependencies. This recipe makes no changes to any source file.,1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""scope"",""type"":""Scope"",""displayName"":""Scope"",""description"":""The scope of the dependencies to include in the report.Defaults to \""Compile\"""",""example"":""Compile"",""valid"":[""Compile"",""Runtime"",""TestRuntime""]},{""name"":""includeTransitive"",""type"":""boolean"",""displayName"":""Include transitive dependencies"",""description"":""Whether or not to include transitive dependencies in the report. Defaults to including only direct dependencies.Defaults to false."",""example"":""true"",""value"":false},{""name"":""validateResolvable"",""type"":""boolean"",""displayName"":""Validate dependencies are resolvable"",""description"":""When enabled the recipe will attempt to download every dependency it encounters, reporting on any failures. This can be useful for identifying dependencies that have become unavailable since an LST was produced.Defaults to false."",""example"":""true"",""valid"":[""true"",""false""],""value"":false}]","[{""name"":""org.openrewrite.java.dependencies.table.DependencyListReport"",""displayName"":""Dependency report"",""instanceName"":""Dependency report"",""description"":""Lists all Gradle and Maven dependencies"",""columns"":[{""name"":""buildTool"",""type"":""String"",""displayName"":""Build tool"",""description"":""The build tool used to manage dependencies (Gradle or Maven).""},{""name"":""groupId"",""type"":""String"",""displayName"":""Group id"",""description"":""The Group ID of the Gradle project or Maven module requesting the dependency.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The Artifact ID of the Gradle project or Maven module requesting the dependency.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of Gradle project or Maven module requesting the dependency.""},{""name"":""dependencyGroupId"",""type"":""String"",""displayName"":""Dependency group id"",""description"":""The Group ID of the dependency.""},{""name"":""dependencyArtifactId"",""type"":""String"",""displayName"":""Dependency artifact id"",""description"":""The Artifact ID of the dependency.""},{""name"":""dependencyVersion"",""type"":""String"",""displayName"":""Dependency version"",""description"":""The version of the dependency.""},{""name"":""direct"",""type"":""boolean"",""displayName"":""Direct Dependency"",""description"":""When `true` the project directly depends on the dependency. When `false` the project depends on the dependency transitively through at least one direct dependency.""},{""name"":""resolutionFailure"",""type"":""String"",""displayName"":""Resolution failure"",""description"":""The reason why the dependency could not be resolved. Blank when resolution was not attempted.""}]},{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" +maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.DependencyResolutionDiagnostic,Dependency resolution diagnostic,"Recipes which manipulate dependencies must be able to successfully access the artifact repositories and resolve dependencies from them. This recipe produces two data tables used to understand the state of dependency resolution. + +The Repository accessibility report lists all the artifact repositories known to the project and whether respond to network access. The network access is attempted while the recipe is run and so is representative of current conditions. + +The Gradle dependency configuration errors lists all the dependency configurations that failed to resolve one or more dependencies when the project was parsed. This is representative of conditions at the time the LST was parsed.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupId"",""type"":""String"",""displayName"":""Group ID"",""description"":""The group ID of a dependency to attempt to download from the repository. Default value is \""com.fasterxml.jackson.core\"". If this dependency is not found in the repository the error will be noted in the report. There is no need to specify an alternate value for this parameter unless the repository is known not to contain jackson-core."",""example"":""com.fasterxml.jackson.core""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact ID"",""description"":""The artifact ID of a dependency to attempt to download from the repository. Default value is \""jackson-core\"". If this dependency is not found in the repository the error will be noted in the report. There is no need to specify an alternate value for this parameter unless the repository is known not to contain jackson-core."",""example"":""jackson-core""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of a dependency to attempt to download from the repository. Default value is \""2.16.0\"". If this dependency is not found in the repository the error will be noted in the report. There is no need to specify an alternate value for this parameter unless the repository is known not to contain jackson-core."",""example"":""2.16.0""}]","[{""name"":""org.openrewrite.java.dependencies.table.RepositoryAccessibilityReport"",""displayName"":""Repository accessibility report"",""instanceName"":""Repository accessibility report"",""description"":""Listing of all dependency repositories and whether they are accessible."",""columns"":[{""name"":""uri"",""type"":""String"",""displayName"":""Repository URI"",""description"":""The URI of the repository""},{""name"":""pingExceptionType"",""type"":""String"",""displayName"":""Ping exception type"",""description"":""Empty if the repository responded to a ping. Otherwise, the type of exception encountered when attempting to access the repository.""},{""name"":""pingExceptionMessage"",""type"":""String"",""displayName"":""Ping error message"",""description"":""Empty if the repository was accessible. Otherwise, the error message encountered when attempting to access the repository.""},{""name"":""pingHttpCode"",""type"":""Integer"",""displayName"":""Ping HTTP code"",""description"":""The HTTP response code returned by the repository. May be empty for non-HTTP repositories.""},{""name"":""dependencyResolveExceptionType"",""type"":""String"",""displayName"":""Dependency resolution exception type"",""description"":""Empty if ping failed, or if the repository successfully downloaded the specified dependency. Otherwise, the type of exception encountered when attempting to access the repository.""},{""name"":""dependencyResolveExceptionMessage"",""type"":""String"",""displayName"":""Dependency resolution error message"",""description"":""Empty if ping failed, or if the repository successfully downloaded the specified dependency. Otherwise, the error message encountered when attempting to access the repository.""}]},{""name"":""org.openrewrite.java.dependencies.table.GradleDependencyConfigurationErrors"",""displayName"":""Gradle dependency configuration errors"",""instanceName"":""Gradle dependency configuration errors"",""description"":""Records Gradle dependency configurations which failed to resolve during parsing. Partial success/failure is common, a failure in this list does not mean that every dependency failed to resolve."",""columns"":[{""name"":""projectPath"",""type"":""String"",""displayName"":""Project path"",""description"":""The path of the project which contains the dependency configuration.""},{""name"":""configurationName"",""type"":""String"",""displayName"":""Configuration name"",""description"":""The name of the dependency configuration which failed to resolve.""},{""name"":""exceptionType"",""type"":""String"",""displayName"":""Exception type"",""description"":""The type of exception encountered when attempting to resolve the dependency configuration.""},{""name"":""exceptionMessage"",""type"":""String"",""displayName"":""Error message"",""description"":""The error message encountered when attempting to resolve the dependency configuration.""}]}]" +maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.FindDependency,Find Maven and Gradle dependencies,Finds direct dependencies declared in Maven and Gradle build files. This does *not* search transitive dependencies. To detect both direct and transitive dependencies use `org.openrewrite.java.dependencies.DependencyInsight` This recipe works for both Maven and Gradle projects.,1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupId"",""type"":""String"",""displayName"":""Group ID"",""description"":""The first part of a dependency coordinate identifying its publisher."",""example"":""com.google.guava"",""required"":true},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact ID"",""description"":""The second part of a dependency coordinate uniquely identifying it among artifacts from the same publisher."",""example"":""guava"",""required"":true},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""An exact version number or node-style semver selector used to select the version number."",""example"":""3.0.0""},{""name"":""versionPattern"",""type"":""String"",""displayName"":""Version pattern"",""description"":""Allows version selection to be extended beyond the original Node Semver semantics. So for example, setting 'version' to \""25-29\"" can be paired with a metadata pattern of \""-jre\"" to select Guava 29.0-jre"",""example"":""-jre""},{""name"":""configuration"",""type"":""String"",""displayName"":""Configuration"",""description"":""For Gradle only, the dependency configuration to search for dependencies in. If omitted then all configurations will be searched."",""example"":""api""}]", maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.FindRepositoryOrder,Maven repository order,Determine the order in which dependencies will be resolved for each `pom.xml` or `build.gradle` based on its defined repositories and effective settings.,1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenRepositoryOrder"",""displayName"":""Maven repository order"",""instanceName"":""Maven repository order"",""description"":""The order in which dependencies will be resolved for each `pom.xml` based on its defined repositories and effective `settings.xml`."",""columns"":[{""name"":""id"",""type"":""String"",""displayName"":""Repository ID"",""description"":""The ID of the repository. Note that projects may define the same physical repository with different IDs.""},{""name"":""uri"",""type"":""String"",""displayName"":""Repository URI"",""description"":""The URI of the repository.""},{""name"":""knownToExist"",""type"":""boolean"",""displayName"":""Known to exist"",""description"":""If the repository is provably reachable. If false, does not guarantee that the repository does not exist.""},{""name"":""rank"",""type"":""int"",""displayName"":""Rank"",""description"":""The index order of this repository in the repository list.""}]}]" +maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.RelocatedDependencyCheck,Find relocated dependencies,"Find Maven and Gradle dependencies and Maven plugins that have relocated to a new `groupId` or `artifactId`. Relocation information comes from the [oga-maven-plugin](https://github.com/jonathanlermitage/oga-maven-plugin/) maintained by Jonathan Lermitage, Filipe Roque and others. + +This recipe makes no changes to any source file by default. Add `changeDependencies=true` to change dependencies, but note that you might need to run additional recipes to update imports and adopt other breaking changes.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""changeDependencies"",""type"":""Boolean"",""displayName"":""Change dependencies"",""description"":""Whether to change dependencies to their relocated groupId and artifactId.""}]","[{""name"":""org.openrewrite.java.dependencies.table.RelocatedDependencyReport"",""displayName"":""Relocated dependencies"",""instanceName"":""Relocated dependencies"",""description"":""A list of dependencies in use that have relocated."",""columns"":[{""name"":""dependencyGroupId"",""type"":""String"",""displayName"":""Dependency group id"",""description"":""The Group ID of the dependency in use.""},{""name"":""dependencyArtifactId"",""type"":""String"",""displayName"":""Dependency artifact id"",""description"":""The Artifact ID of the dependency in use.""},{""name"":""relocatedGroupId"",""type"":""String"",""displayName"":""Relocated dependency group id"",""description"":""The Group ID of the relocated dependency.""},{""name"":""relocatedArtifactId"",""type"":""String"",""displayName"":""Relocated ependency artifact id"",""description"":""The Artifact ID of the relocated dependency.""},{""name"":""context"",""type"":""String"",""displayName"":""Context"",""description"":""Context for the relocation, if any.""}]}]" +maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.RemoveDependency,Remove a Gradle or Maven dependency,"For Gradle project, removes a single dependency from the dependencies section of the `build.gradle`. +For Maven project, removes a single dependency from the `` section of the pom.xml.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupId"",""type"":""String"",""displayName"":""Group ID"",""description"":""The first part of a dependency coordinate `com.google.guava:guava:VERSION`. This can be a glob expression."",""example"":""com.fasterxml.jackson*"",""required"":true},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact ID"",""description"":""The second part of a dependency coordinate `com.google.guava:guava:VERSION`. This can be a glob expression."",""example"":""jackson-module*"",""required"":true},{""name"":""unlessUsing"",""type"":""String"",""displayName"":""Unless using"",""description"":""Do not remove if type is in use. Supports glob expressions."",""example"":""org.aspectj.lang.*""},{""name"":""configuration"",""type"":""String"",""displayName"":""The dependency configuration"",""description"":""The dependency configuration to remove from."",""example"":""api""},{""name"":""scope"",""type"":""String"",""displayName"":""Scope"",""description"":""Only remove dependencies if they are in this scope. If 'runtime', this willalso remove dependencies in the 'compile' scope because 'compile' dependencies are part of the runtime dependency set"",""example"":""compile"",""valid"":[""compile"",""test"",""runtime"",""provided""]}]", maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.RemoveRedundantDependencies,Remove redundant explicit dependencies,"Remove explicit dependencies that are already provided transitively by a specified dependency. This recipe downloads and resolves the parent dependency's POM to determine its true transitive dependencies, allowing it to detect redundancies even when both dependencies are explicitly declared.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupId"",""type"":""String"",""displayName"":""Group ID"",""description"":""The first part of a dependency coordinate `com.google.guava:guava:VERSION` of the parent dependency. This can be a glob expression."",""example"":""com.fasterxml.jackson.core"",""required"":true},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact ID"",""description"":""The second part of a dependency coordinate `com.google.guava:guava:VERSION` of the parent dependency. This can be a glob expression."",""example"":""jackson-databind"",""required"":true}]", +maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.RepositoryLatencyDiagnostic,Artifact repository latency diagnostic,"Measures how long it takes to fetch a Maven metadata file from every artifact repository known to the build, to help diagnose slow or flaky repository connectivity. For each effective repository it requests `maven-metadata.xml` for a configurable artifact several times and records the HTTP status of each request and the latency deciles in the `Repository response latency` data table. + +Requests are issued through the same network path a real recipe run uses, so in a Moderne deployment they traverse the worker, gateway and connector before reaching the repository. The recipe only observes end-to-end time, so it additionally fires cache-busting requests and reads any `Server-Timing` response header to attribute latency to the repository versus Moderne infrastructure as far as the available signals allow.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupId"",""type"":""String"",""displayName"":""Group ID"",""description"":""The group ID of the artifact whose `maven-metadata.xml` is fetched. Default value is \""com.fasterxml.jackson.core\""."",""example"":""com.fasterxml.jackson.core""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact ID"",""description"":""The artifact ID of the artifact whose `maven-metadata.xml` is fetched. Default value is \""jackson-core\""."",""example"":""jackson-core""},{""name"":""requestsPerRepository"",""type"":""Integer"",""displayName"":""Requests per repository"",""description"":""How many times to request the metadata from each repository. Default value is 10. The data table always reports the first 10 request statuses and 10 latency deciles."",""example"":""10""}]","[{""name"":""org.openrewrite.java.dependencies.table.RepositoryResponseLatency"",""displayName"":""Repository response latency"",""instanceName"":""Repository response latency"",""description"":""One row per effective artifact repository, recording the latency of repeatedly fetching a Maven metadata file. Because the recipe only observes end-to-end time, the derived columns attribute latency to a layer (Moderne infrastructure vs. the repository) as best they can from status codes, the connector cache hit/miss cliff, and any Server-Timing header returned by the tunnel."",""columns"":[{""name"":""repositoryUri"",""type"":""String"",""displayName"":""Repository URI"",""description"":""The effective (post-mirror, normalized) URI of the repository.""},{""name"":""metadataUri"",""type"":""String"",""displayName"":""Maven metadata URI"",""description"":""The exact `maven-metadata.xml` URL that was probed.""},{""name"":""groupArtifact"",""type"":""String"",""displayName"":""Group:artifact probed"",""description"":""The `groupId:artifactId` whose metadata was requested.""},{""name"":""httpResponseCount"",""type"":""Integer"",""displayName"":""HTTP response count"",""description"":""How many of the probes returned any HTTP status (2xx-5xx). The remainder failed at the transport layer (timeout, unknown host, connection refused).""},{""name"":""connectionFailureCount"",""type"":""Integer"",""displayName"":""Connection failure count"",""description"":""How many probes failed before receiving an HTTP status (timeout, unknown host, refused).""},{""name"":""status1"",""type"":""String"",""displayName"":""Request 1 status"",""description"":""HTTP status, or the transport exception, of probe 1.""},{""name"":""status2"",""type"":""String"",""displayName"":""Request 2 status"",""description"":""HTTP status, or the transport exception, of probe 2.""},{""name"":""status3"",""type"":""String"",""displayName"":""Request 3 status"",""description"":""HTTP status, or the transport exception, of probe 3.""},{""name"":""status4"",""type"":""String"",""displayName"":""Request 4 status"",""description"":""HTTP status, or the transport exception, of probe 4.""},{""name"":""status5"",""type"":""String"",""displayName"":""Request 5 status"",""description"":""HTTP status, or the transport exception, of probe 5.""},{""name"":""status6"",""type"":""String"",""displayName"":""Request 6 status"",""description"":""HTTP status, or the transport exception, of probe 6.""},{""name"":""status7"",""type"":""String"",""displayName"":""Request 7 status"",""description"":""HTTP status, or the transport exception, of probe 7.""},{""name"":""status8"",""type"":""String"",""displayName"":""Request 8 status"",""description"":""HTTP status, or the transport exception, of probe 8.""},{""name"":""status9"",""type"":""String"",""displayName"":""Request 9 status"",""description"":""HTTP status, or the transport exception, of probe 9.""},{""name"":""status10"",""type"":""String"",""displayName"":""Request 10 status"",""description"":""HTTP status, or the transport exception, of probe 10.""},{""name"":""latencyDecile1Ms"",""type"":""Long"",""displayName"":""Latency decile 1 (ms)"",""description"":""10th percentile latency of the responding probes (fastest).""},{""name"":""latencyDecile2Ms"",""type"":""Long"",""displayName"":""Latency decile 2 (ms)"",""description"":""20th percentile latency of the responding probes.""},{""name"":""latencyDecile3Ms"",""type"":""Long"",""displayName"":""Latency decile 3 (ms)"",""description"":""30th percentile latency of the responding probes.""},{""name"":""latencyDecile4Ms"",""type"":""Long"",""displayName"":""Latency decile 4 (ms)"",""description"":""40th percentile latency of the responding probes.""},{""name"":""latencyDecile5Ms"",""type"":""Long"",""displayName"":""Latency decile 5 (ms)"",""description"":""50th percentile (median) latency of the responding probes.""},{""name"":""latencyDecile6Ms"",""type"":""Long"",""displayName"":""Latency decile 6 (ms)"",""description"":""60th percentile latency of the responding probes.""},{""name"":""latencyDecile7Ms"",""type"":""Long"",""displayName"":""Latency decile 7 (ms)"",""description"":""70th percentile latency of the responding probes.""},{""name"":""latencyDecile8Ms"",""type"":""Long"",""displayName"":""Latency decile 8 (ms)"",""description"":""80th percentile latency of the responding probes.""},{""name"":""latencyDecile9Ms"",""type"":""Long"",""displayName"":""Latency decile 9 (ms)"",""description"":""90th percentile latency of the responding probes.""},{""name"":""latencyDecile10Ms"",""type"":""Long"",""displayName"":""Latency decile 10 (ms)"",""description"":""100th percentile latency of the responding probes (slowest).""},{""name"":""coldStartMs"",""type"":""Long"",""displayName"":""Cold start latency (ms)"",""description"":""Latency of the first counted probe. Includes connection setup and, when the connector cache is cold, the full connector-to-repository fetch.""},{""name"":""warmMedianMs"",""type"":""Long"",""displayName"":""Warm median latency (ms)"",""description"":""Median latency of probes 2..N. When the connector caches metadata these are cache hits, so this approximates the Moderne-internal round trip (worker->gateway->connector) with no repository fetch.""},{""name"":""cacheImmuneMedianMs"",""type"":""Long"",""displayName"":""Cache-immune median latency (ms)"",""description"":""Median latency of the cache-busting probes (a unique query string forces the connector to reach the repository every time), i.e. the true full-path latency.""},{""name"":""estimatedRepoLatencyMs"",""type"":""Long"",""displayName"":""Estimated connector->repository latency (ms)"",""description"":""Cache-immune median minus warm median. When the warm probes are connector cache hits, this isolates the connector-to-repository hop. Meaningless (near zero) when the connector is not caching — see the `Warm requests served from connector cache` column.""},{""name"":""jitterMs"",""type"":""Long"",""displayName"":""Latency jitter p90-p10 (ms)"",""description"":""Spread between the 90th and 10th percentile of the responding probes. High jitter points to intermittent causes (GC, RSocket head-of-line blocking, connector reconnects, CDN variance).""},{""name"":""warmServedFromConnectorCache"",""type"":""Boolean"",""displayName"":""Warm requests served from connector cache"",""description"":""True when the warm probes returned quickly with empty response headers, the signature of a connector-side Maven cache hit. When false, every probe is a full-path request.""},{""name"":""serverTimingRepositoryMs"",""type"":""Long"",""displayName"":""Server-Timing repository (ms)"",""description"":""Connector-measured time spent fetching from the repository, read from the `Server-Timing` response header. Empty until the gateway/connector emit it.""},{""name"":""serverTimingConnectorMs"",""type"":""Long"",""displayName"":""Server-Timing connector (ms)"",""description"":""Connector-measured in-connector processing time, read from the `Server-Timing` response header. Empty until the gateway/connector emit it.""},{""name"":""serverTimingRsocketMs"",""type"":""Long"",""displayName"":""Server-Timing RSocket (ms)"",""description"":""Gateway-measured gateway<->connector RSocket exchange time, read from the `Server-Timing` response header. Empty until the gateway/connector emit it.""},{""name"":""upstreamServer"",""type"":""String"",""displayName"":""Upstream server header"",""description"":""The `Server`/`Via`/`X-Cache` response headers of a cache-immune probe, revealing a CDN or proxy in front of the repository. Empty for connector cache hits (which strip headers).""},{""name"":""probableBottleneck"",""type"":""String"",""displayName"":""Probable bottleneck"",""description"":""A heuristic classification of where the latency or failure most likely originates.""},{""name"":""notes"",""type"":""String"",""displayName"":""Notes"",""description"":""Free-form detail: transport exceptions, whether a mirror was applied, unreachable reasons.""}]}]" maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.UpgradeDependencyVersion,Upgrade Gradle or Maven dependency versions,"For Gradle projects, upgrade the version of a dependency in a `build.gradle` file. Supports updating dependency declarations of various forms: * `String` notation: `""group:artifact:version""` * `Map` notation: `group: 'group', name: 'artifact', version: 'version'` It is possible to update version numbers which are defined earlier in the same file in variable declarations. For Maven projects, upgrade the version of a dependency by specifying a group ID and (optionally) an artifact ID using Node Semver advanced range selectors, allowing more precise control over version updates to patch or minor releases.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupId"",""type"":""String"",""displayName"":""Group ID"",""description"":""The first part of a dependency coordinate `com.google.guava:guava:VERSION`. This can be a glob expression."",""example"":""com.fasterxml.jackson*"",""required"":true},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact ID"",""description"":""The second part of a dependency coordinate `com.google.guava:guava:VERSION`. This can be a glob expression."",""example"":""jackson-module*"",""required"":true},{""name"":""newVersion"",""type"":""String"",""displayName"":""New version"",""description"":""An exact version number or node-style semver selector used to select the version number. "",""example"":""29.X"",""required"":true},{""name"":""versionPattern"",""type"":""String"",""displayName"":""Version pattern"",""description"":""Allows version selection to be extended beyond the original Node Semver semantics. So for example,Setting 'version' to \""25-29\"" can be paired with a metadata pattern of \""-jre\"" to select Guava 29.0-jre"",""example"":""-jre""},{""name"":""overrideManagedVersion"",""type"":""Boolean"",""displayName"":""Override managed version"",""description"":""For Maven project only, This flag can be set to explicitly override a managed dependency's version. The default for this flag is `false`.""},{""name"":""retainVersions"",""type"":""List"",""displayName"":""Retain versions"",""description"":""For Maven project only, accepts a list of GAVs. For each GAV, if it is a project direct dependency, and it is removed from dependency management after the changes from this recipe, then it will be retained with an explicit version. The version can be omitted from the GAV to use the old value from dependency management."",""example"":""com.jcraft:jsch""}]", -maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.RemoveDependency,Remove a Gradle or Maven dependency,"For Gradle project, removes a single dependency from the dependencies section of the `build.gradle`. -For Maven project, removes a single dependency from the `` section of the pom.xml.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupId"",""type"":""String"",""displayName"":""Group ID"",""description"":""The first part of a dependency coordinate `com.google.guava:guava:VERSION`. This can be a glob expression."",""example"":""com.fasterxml.jackson*"",""required"":true},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact ID"",""description"":""The second part of a dependency coordinate `com.google.guava:guava:VERSION`. This can be a glob expression."",""example"":""jackson-module*"",""required"":true},{""name"":""unlessUsing"",""type"":""String"",""displayName"":""Unless using"",""description"":""Do not remove if type is in use. Supports glob expressions."",""example"":""org.aspectj.lang.*""},{""name"":""configuration"",""type"":""String"",""displayName"":""The dependency configuration"",""description"":""The dependency configuration to remove from."",""example"":""api""},{""name"":""scope"",""type"":""String"",""displayName"":""Scope"",""description"":""Only remove dependencies if they are in this scope. If 'runtime', this willalso remove dependencies in the 'compile' scope because 'compile' dependencies are part of the runtime dependency set"",""example"":""compile"",""valid"":[""compile"",""test"",""runtime"",""provided""]}]", -maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.FindDependency,Find Maven and Gradle dependencies,Finds direct dependencies declared in Maven and Gradle build files. This does *not* search transitive dependencies. To detect both direct and transitive dependencies use `org.openrewrite.java.dependencies.DependencyInsight` This recipe works for both Maven and Gradle projects.,1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupId"",""type"":""String"",""displayName"":""Group ID"",""description"":""The first part of a dependency coordinate identifying its publisher."",""example"":""com.google.guava"",""required"":true},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact ID"",""description"":""The second part of a dependency coordinate uniquely identifying it among artifacts from the same publisher."",""example"":""guava"",""required"":true},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""An exact version number or node-style semver selector used to select the version number."",""example"":""3.0.0""},{""name"":""versionPattern"",""type"":""String"",""displayName"":""Version pattern"",""description"":""Allows version selection to be extended beyond the original Node Semver semantics. So for example, setting 'version' to \""25-29\"" can be paired with a metadata pattern of \""-jre\"" to select Guava 29.0-jre"",""example"":""-jre""},{""name"":""configuration"",""type"":""String"",""displayName"":""Configuration"",""description"":""For Gradle only, the dependency configuration to search for dependencies in. If omitted then all configurations will be searched."",""example"":""api""}]", -maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.DependencyList,Dependency report,Emits a data table detailing all Gradle and Maven dependencies. This recipe makes no changes to any source file.,1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""scope"",""type"":""Scope"",""displayName"":""Scope"",""description"":""The scope of the dependencies to include in the report.Defaults to \""Compile\"""",""example"":""Compile"",""valid"":[""Compile"",""Runtime"",""TestRuntime""]},{""name"":""includeTransitive"",""type"":""boolean"",""displayName"":""Include transitive dependencies"",""description"":""Whether or not to include transitive dependencies in the report. Defaults to including only direct dependencies.Defaults to false."",""example"":""true"",""value"":false},{""name"":""validateResolvable"",""type"":""boolean"",""displayName"":""Validate dependencies are resolvable"",""description"":""When enabled the recipe will attempt to download every dependency it encounters, reporting on any failures. This can be useful for identifying dependencies that have become unavailable since an LST was produced.Defaults to false."",""example"":""true"",""valid"":[""true"",""false""],""value"":false}]","[{""name"":""org.openrewrite.java.dependencies.table.DependencyListReport"",""displayName"":""Dependency report"",""instanceName"":""Dependency report"",""description"":""Lists all Gradle and Maven dependencies"",""columns"":[{""name"":""buildTool"",""type"":""String"",""displayName"":""Build tool"",""description"":""The build tool used to manage dependencies (Gradle or Maven).""},{""name"":""groupId"",""type"":""String"",""displayName"":""Group id"",""description"":""The Group ID of the Gradle project or Maven module requesting the dependency.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The Artifact ID of the Gradle project or Maven module requesting the dependency.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of Gradle project or Maven module requesting the dependency.""},{""name"":""dependencyGroupId"",""type"":""String"",""displayName"":""Dependency group id"",""description"":""The Group ID of the dependency.""},{""name"":""dependencyArtifactId"",""type"":""String"",""displayName"":""Dependency artifact id"",""description"":""The Artifact ID of the dependency.""},{""name"":""dependencyVersion"",""type"":""String"",""displayName"":""Dependency version"",""description"":""The version of the dependency.""},{""name"":""direct"",""type"":""boolean"",""displayName"":""Direct Dependency"",""description"":""When `true` the project directly depends on the dependency. When `false` the project depends on the dependency transitively through at least one direct dependency.""},{""name"":""resolutionFailure"",""type"":""String"",""displayName"":""Resolution failure"",""description"":""The reason why the dependency could not be resolved. Blank when resolution was not attempted.""}]},{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" -maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.RelocatedDependencyCheck,Find relocated dependencies,"Find Maven and Gradle dependencies and Maven plugins that have relocated to a new `groupId` or `artifactId`. Relocation information comes from the [oga-maven-plugin](https://github.com/jonathanlermitage/oga-maven-plugin/) maintained by Jonathan Lermitage, Filipe Roque and others. - -This recipe makes no changes to any source file by default. Add `changeDependencies=true` to change dependencies, but note that you might need to run additional recipes to update imports and adopt other breaking changes.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""changeDependencies"",""type"":""Boolean"",""displayName"":""Change dependencies"",""description"":""Whether to change dependencies to their relocated groupId and artifactId.""}]","[{""name"":""org.openrewrite.java.dependencies.table.RelocatedDependencyReport"",""displayName"":""Relocated dependencies"",""instanceName"":""Relocated dependencies"",""description"":""A list of dependencies in use that have relocated."",""columns"":[{""name"":""dependencyGroupId"",""type"":""String"",""displayName"":""Dependency group id"",""description"":""The Group ID of the dependency in use.""},{""name"":""dependencyArtifactId"",""type"":""String"",""displayName"":""Dependency artifact id"",""description"":""The Artifact ID of the dependency in use.""},{""name"":""relocatedGroupId"",""type"":""String"",""displayName"":""Relocated dependency group id"",""description"":""The Group ID of the relocated dependency.""},{""name"":""relocatedArtifactId"",""type"":""String"",""displayName"":""Relocated ependency artifact id"",""description"":""The Artifact ID of the relocated dependency.""},{""name"":""context"",""type"":""String"",""displayName"":""Context"",""description"":""Context for the relocation, if any.""}]}]" -maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.DependencyInsight,Dependency insight for Gradle and Maven,"Finds dependencies, including transitive dependencies, in both Gradle and Maven projects. Matches within all Gradle dependency configurations and Maven scopes.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupIdPattern"",""type"":""String"",""displayName"":""Group pattern"",""description"":""Group ID glob pattern used to match dependencies."",""example"":""com.fasterxml.jackson*"",""required"":true},{""name"":""artifactIdPattern"",""type"":""String"",""displayName"":""Artifact pattern"",""description"":""Artifact ID glob pattern used to match dependencies."",""example"":""jackson-*"",""required"":true},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""Match only dependencies with the specified version. Node-style [version selectors](https://docs.openrewrite.org/reference/dependency-version-selectors) may be used. All versions are searched by default."",""example"":""1.x""},{""name"":""scope"",""type"":""String"",""displayName"":""Scope"",""description"":""Match dependencies with the specified Maven scope. All scopes are searched by default."",""example"":""compile"",""valid"":[""compile"",""test"",""runtime"",""provided"",""system""]}]","[{""name"":""org.openrewrite.maven.table.DependenciesInUse"",""displayName"":""Dependencies in use"",""instanceName"":""Dependencies in use"",""description"":""Direct and transitive dependencies in use."",""columns"":[{""name"":""projectName"",""type"":""String"",""displayName"":""Project name"",""description"":""The name of the project that contains the dependency.""},{""name"":""sourceSet"",""type"":""String"",""displayName"":""Source set"",""description"":""The source set that contains the dependency.""},{""name"":""groupId"",""type"":""String"",""displayName"":""Group"",""description"":""The first part of a dependency coordinate `com.google.guava:guava:VERSION`.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact"",""description"":""The second part of a dependency coordinate `com.google.guava:guava:VERSION`.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The resolved version.""},{""name"":""datedSnapshotVersion"",""type"":""String"",""displayName"":""Dated snapshot version"",""description"":""The resolved dated snapshot version or `null` if this dependency is not a snapshot.""},{""name"":""scope"",""type"":""String"",""displayName"":""Scope"",""description"":""Dependency scope. This will be `compile` if the dependency is direct and a scope is not explicitly specified in the POM.""},{""name"":""count"",""type"":""Integer"",""displayName"":""Count"",""description"":""How many times does this dependency appear.""}]}]" -maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.DependencyResolutionDiagnostic,Dependency resolution diagnostic,"Recipes which manipulate dependencies must be able to successfully access the artifact repositories and resolve dependencies from them. This recipe produces two data tables used to understand the state of dependency resolution. - -The Repository accessibility report lists all the artifact repositories known to the project and whether respond to network access. The network access is attempted while the recipe is run and so is representative of current conditions. - -The Gradle dependency configuration errors lists all the dependency configurations that failed to resolve one or more dependencies when the project was parsed. This is representative of conditions at the time the LST was parsed.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupId"",""type"":""String"",""displayName"":""Group ID"",""description"":""The group ID of a dependency to attempt to download from the repository. Default value is \""com.fasterxml.jackson.core\"". If this dependency is not found in the repository the error will be noted in the report. There is no need to specify an alternate value for this parameter unless the repository is known not to contain jackson-core."",""example"":""com.fasterxml.jackson.core""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact ID"",""description"":""The artifact ID of a dependency to attempt to download from the repository. Default value is \""jackson-core\"". If this dependency is not found in the repository the error will be noted in the report. There is no need to specify an alternate value for this parameter unless the repository is known not to contain jackson-core."",""example"":""jackson-core""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of a dependency to attempt to download from the repository. Default value is \""2.16.0\"". If this dependency is not found in the repository the error will be noted in the report. There is no need to specify an alternate value for this parameter unless the repository is known not to contain jackson-core."",""example"":""2.16.0""}]","[{""name"":""org.openrewrite.java.dependencies.table.RepositoryAccessibilityReport"",""displayName"":""Repository accessibility report"",""instanceName"":""Repository accessibility report"",""description"":""Listing of all dependency repositories and whether they are accessible."",""columns"":[{""name"":""uri"",""type"":""String"",""displayName"":""Repository URI"",""description"":""The URI of the repository""},{""name"":""pingExceptionType"",""type"":""String"",""displayName"":""Ping exception type"",""description"":""Empty if the repository responded to a ping. Otherwise, the type of exception encountered when attempting to access the repository.""},{""name"":""pingExceptionMessage"",""type"":""String"",""displayName"":""Ping error message"",""description"":""Empty if the repository was accessible. Otherwise, the error message encountered when attempting to access the repository.""},{""name"":""pingHttpCode"",""type"":""Integer"",""displayName"":""Ping HTTP code"",""description"":""The HTTP response code returned by the repository. May be empty for non-HTTP repositories.""},{""name"":""dependencyResolveExceptionType"",""type"":""String"",""displayName"":""Dependency resolution exception type"",""description"":""Empty if ping failed, or if the repository successfully downloaded the specified dependency. Otherwise, the type of exception encountered when attempting to access the repository.""},{""name"":""dependencyResolveExceptionMessage"",""type"":""String"",""displayName"":""Dependency resolution error message"",""description"":""Empty if ping failed, or if the repository successfully downloaded the specified dependency. Otherwise, the error message encountered when attempting to access the repository.""}]},{""name"":""org.openrewrite.java.dependencies.table.GradleDependencyConfigurationErrors"",""displayName"":""Gradle dependency configuration errors"",""instanceName"":""Gradle dependency configuration errors"",""description"":""Records Gradle dependency configurations which failed to resolve during parsing. Partial success/failure is common, a failure in this list does not mean that every dependency failed to resolve."",""columns"":[{""name"":""projectPath"",""type"":""String"",""displayName"":""Project path"",""description"":""The path of the project which contains the dependency configuration.""},{""name"":""configurationName"",""type"":""String"",""displayName"":""Configuration name"",""description"":""The name of the dependency configuration which failed to resolve.""},{""name"":""exceptionType"",""type"":""String"",""displayName"":""Exception type"",""description"":""The type of exception encountered when attempting to resolve the dependency configuration.""},{""name"":""exceptionMessage"",""type"":""String"",""displayName"":""Error message"",""description"":""The error message encountered when attempting to resolve the dependency configuration.""}]}]" -maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.AddDependency,Add Gradle or Maven dependency,"For a Gradle project, add a gradle dependency to a `build.gradle` file in the correct configuration based on where it is used. Or For a maven project, Add a Maven dependency to a `pom.xml` file in the correct scope based on where it is used.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupId"",""type"":""String"",""displayName"":""Group ID"",""description"":""The first part of a dependency coordinate `com.google.guava:guava:VERSION`."",""example"":""com.google.guava"",""required"":true},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact ID"",""description"":""The second part of a dependency coordinate `com.google.guava:guava:VERSION`"",""example"":""guava"",""required"":true},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""An exact version number or node-style semver selector used to select the version number."",""example"":""29.X""},{""name"":""versionPattern"",""type"":""String"",""displayName"":""Version pattern"",""description"":""Allows version selection to be extended beyond the original Node Semver semantics. So for example, Setting 'version' to \""25-29\"" can be paired with a metadata pattern of \""-jre\"" to select Guava 29.0-jre"",""example"":""-jre""},{""name"":""onlyIfUsing"",""type"":""String"",""displayName"":""Only if using"",""description"":""Used to determine if the dependency will be added and in which scope it should be placed."",""example"":""org.junit.jupiter.api.*""},{""name"":""classifier"",""type"":""String"",""displayName"":""Classifier"",""description"":""A classifier to add. Commonly used to select variants of a library."",""example"":""test""},{""name"":""familyPattern"",""type"":""String"",""displayName"":""Family pattern"",""description"":""A pattern, applied to groupIds, used to determine which other dependencies should have aligned version numbers. Accepts '*' as a wildcard character."",""example"":""com.fasterxml.jackson*""},{""name"":""extension"",""type"":""String"",""displayName"":""Extension"",""description"":""For Gradle only, The extension of the dependency to add. If omitted Gradle defaults to assuming the type is \""jar\""."",""example"":""jar""},{""name"":""configuration"",""type"":""String"",""displayName"":""Gradle configuration"",""description"":""The Gradle dependency configuration name within which to place the dependency. When omitted the configuration will be determined by the Maven scope parameter. If that parameter is also omitted, configuration will be determined based on where types matching `onlyIfUsing` appear in source code."",""example"":""implementation""},{""name"":""scope"",""type"":""String"",""displayName"":""Maven scope"",""description"":""The Maven scope within which to place the dependency. When omitted scope will be determined based on where types matching `onlyIfUsing` appear in source code."",""example"":""runtime"",""valid"":[""compile"",""provided"",""runtime"",""test""]},{""name"":""releasesOnly"",""type"":""Boolean"",""displayName"":""Releases only"",""description"":""For Maven only, Whether to exclude snapshots from consideration when using a semver selector""},{""name"":""type"",""type"":""String"",""displayName"":""Type"",""description"":""For Maven only, The type of dependency to add. If omitted Maven defaults to assuming the type is \""jar\""."",""example"":""jar"",""valid"":[""jar"",""pom"",""war""]},{""name"":""optional"",""type"":""Boolean"",""displayName"":""Optional"",""description"":""Set the value of the `` tag. No `` tag will be added when this is `null`.""},{""name"":""acceptTransitive"",""type"":""Boolean"",""displayName"":""Accept transitive"",""description"":""Default false. If enabled, the dependency will not be added if it is already on the classpath as a transitive dependency."",""example"":""true""}]", +maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.UpgradeTransitiveDependencyVersion,Upgrade transitive Gradle or Maven dependencies,"Upgrades the version of a transitive dependency in a Maven pom.xml or Gradle build.gradle. Leaves direct dependencies unmodified. Can be paired with the regular Upgrade Dependency Version recipe to upgrade a dependency everywhere, regardless of whether it is direct or transitive.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupId"",""type"":""String"",""displayName"":""Group"",""description"":""The first part of a dependency coordinate 'org.apache.logging.log4j:ARTIFACT_ID:VERSION'."",""example"":""org.apache.logging.log4j"",""required"":true},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact"",""description"":""The second part of a dependency coordinate 'org.apache.logging.log4j:log4j-bom:VERSION'."",""example"":""log4j-bom"",""required"":true},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""An exact version number or node-style semver selector used to select the version number."",""example"":""latest.release"",""required"":true},{""name"":""scope"",""type"":""String"",""displayName"":""Scope"",""description"":""An optional scope to use for the dependency management tag. Relevant only to Maven."",""example"":""import"",""valid"":[""import"",""runtime"",""provided"",""test""]},{""name"":""type"",""type"":""String"",""displayName"":""Type"",""description"":""An optional type to use for the dependency management tag. Relevant only to Maven builds."",""example"":""pom"",""valid"":[""jar"",""pom"",""war""]},{""name"":""classifier"",""type"":""String"",""displayName"":""Classifier"",""description"":""An optional classifier to use for the dependency management tag. Relevant only to Maven."",""example"":""test""},{""name"":""versionPattern"",""type"":""String"",""displayName"":""Version pattern"",""description"":""Allows version selection to be extended beyond the original Node Semver semantics. So for example,Setting 'version' to \""25-29\"" can be paired with a metadata pattern of \""-jre\"" to select 29.0-jre"",""example"":""-jre""},{""name"":""because"",""type"":""String"",""displayName"":""Because"",""description"":""The reason for upgrading the transitive dependency. For example, we could be responding to a vulnerability."",""example"":""CVE-2021-1234""},{""name"":""releasesOnly"",""type"":""Boolean"",""displayName"":""Releases only"",""description"":""Whether to exclude snapshots from consideration when using a semver selector""},{""name"":""onlyIfUsing"",""type"":""String"",""displayName"":""Only if using glob expression for group:artifact"",""description"":""Only add managed dependencies to projects having a dependency matching the expression."",""example"":""org.apache.logging.log4j:log4j*""},{""name"":""addToRootPom"",""type"":""Boolean"",""displayName"":""Add to the root pom"",""description"":""Add to the root pom where root is the eldest parent of the pom within the source set.""}]", maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.search.DoesNotIncludeDependency,Does not include dependency for Gradle and Maven,"A precondition which returns false if visiting a Gradle file / Maven pom which includes the specified dependency in the classpath of some Gradle configuration / Maven scope. For compatibility with multimodule projects, this should most often be applied as a precondition.",1,Search,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupId"",""type"":""String"",""displayName"":""Group"",""description"":""The first part of a dependency coordinate `com.google.guava:guava:VERSION`. Supports glob."",""example"":""com.google.guava"",""required"":true},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact"",""description"":""The second part of a dependency coordinate `com.google.guava:guava:VERSION`. Supports glob."",""example"":""guava"",""required"":true},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""Match only dependencies with the specified resolved version. Node-style [version selectors](https://docs.openrewrite.org/reference/dependency-version-selectors) may be used. All versions are searched by default."",""example"":""1.x""},{""name"":""onlyDirect"",""type"":""Boolean"",""displayName"":""Only direct dependencies"",""description"":""Default false. If enabled, transitive dependencies will not be considered."",""example"":""true""},{""name"":""scope"",""type"":""String"",""displayName"":""Maven scope"",""description"":""Default any. If specified, only the requested scope's classpaths will be checked."",""example"":""compile"",""valid"":[""compile"",""test"",""runtime"",""provided""]},{""name"":""configuration"",""type"":""String"",""displayName"":""Gradle configuration"",""description"":""Match dependencies with the specified configuration. If not specified, all configurations will be searched."",""example"":""compileClasspath""}]", -maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.search.RepositoryHasDependency,Repository has dependency,"Searches for both Gradle and Maven modules that have a dependency matching the specified groupId and artifactId. Places a `SearchResult` marker on all sources within a repository with a matching dependency. This recipe is intended to be used as a precondition for other recipes. For example this could be used to limit the application of a spring boot migration to only projects that use a springframework dependency, limiting unnecessary upgrading. If the search result you want is instead just the build.gradle(.kts) or pom.xml file applying the plugin, use the `FindDependency` recipe instead.",1,Search,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupIdPattern"",""type"":""String"",""displayName"":""Group pattern"",""description"":""Group glob pattern used to match dependencies."",""example"":""com.fasterxml.jackson.module"",""required"":true},{""name"":""artifactIdPattern"",""type"":""String"",""displayName"":""Artifact pattern"",""description"":""Artifact glob pattern used to match dependencies."",""example"":""jackson-module-*"",""required"":true},{""name"":""scope"",""type"":""String"",""displayName"":""Scope"",""description"":""Match dependencies with the specified scope. All scopes are searched by default."",""example"":""compile"",""valid"":[""compile"",""test"",""runtime"",""provided"",""system""]},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""Match only dependencies with the specified version. Node-style [version selectors](https://docs.openrewrite.org/reference/dependency-version-selectors) may be used.All versions are searched by default."",""example"":""1.x""}]", maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.search.FindDuplicateClasses,Find duplicate classes on the classpath,Detects classes that appear in multiple dependencies on the classpath. This is similar to what the Maven duplicate-finder-maven-plugin does. Duplicate classes can cause runtime issues when different versions of the same class are loaded.,1,Search,Dependencies,Java,,,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.java.dependencies.table.DuplicateClassesReport"",""displayName"":""Duplicate classes report"",""instanceName"":""Duplicate classes report"",""description"":""Lists classes that appear in multiple dependencies on the classpath"",""columns"":[{""name"":""projectName"",""type"":""String"",""displayName"":""Project name"",""description"":""The project containing the duplicate.""},{""name"":""sourceSet"",""type"":""String"",""displayName"":""Source set"",""description"":""The source set containing the duplicate (e.g., main, test).""},{""name"":""typeName"",""type"":""String"",""displayName"":""Type name"",""description"":""The fully qualified name of the duplicate class.""},{""name"":""dependency1"",""type"":""String"",""displayName"":""Dependency 1"",""description"":""The first dependency containing the class (group:artifact:version).""},{""name"":""dependency2"",""type"":""String"",""displayName"":""Dependency 2"",""description"":""The second dependency containing the class (group:artifact:version).""},{""name"":""additionalDependencies"",""type"":""String"",""displayName"":""Additional dependencies"",""description"":""Any additional dependencies beyond the first two that also contain this class, comma-separated.""}]}]" maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.search.FindMinimumDependencyVersion,Find the oldest matching dependency version in use,"The oldest dependency version in use is the lowest dependency version in use in any source set of any subproject of a repository. It is possible that, for example, the main source set of a project uses Jackson 2.11, but a test source set uses Jackson 2.16. In this case, the oldest Jackson version in use is Java 2.11.",1,Search,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupIdPattern"",""type"":""String"",""displayName"":""Group pattern"",""description"":""Group ID glob pattern used to match dependencies."",""example"":""com.fasterxml.jackson.module"",""required"":true},{""name"":""artifactIdPattern"",""type"":""String"",""displayName"":""Artifact pattern"",""description"":""Artifact ID glob pattern used to match dependencies."",""example"":""jackson-module-*"",""required"":true},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""Match only dependencies with the specified version. Node-style [version selectors](https://docs.openrewrite.org/reference/dependency-version-selectors) may be used. All versions are searched by default."",""example"":""1.x""}]","[{""name"":""org.openrewrite.maven.table.DependenciesInUse"",""displayName"":""Dependencies in use"",""instanceName"":""Dependencies in use"",""description"":""Direct and transitive dependencies in use."",""columns"":[{""name"":""projectName"",""type"":""String"",""displayName"":""Project name"",""description"":""The name of the project that contains the dependency.""},{""name"":""sourceSet"",""type"":""String"",""displayName"":""Source set"",""description"":""The source set that contains the dependency.""},{""name"":""groupId"",""type"":""String"",""displayName"":""Group"",""description"":""The first part of a dependency coordinate `com.google.guava:guava:VERSION`.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact"",""description"":""The second part of a dependency coordinate `com.google.guava:guava:VERSION`.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The resolved version.""},{""name"":""datedSnapshotVersion"",""type"":""String"",""displayName"":""Dated snapshot version"",""description"":""The resolved dated snapshot version or `null` if this dependency is not a snapshot.""},{""name"":""scope"",""type"":""String"",""displayName"":""Scope"",""description"":""Dependency scope. This will be `compile` if the dependency is direct and a scope is not explicitly specified in the POM.""},{""name"":""count"",""type"":""Integer"",""displayName"":""Count"",""description"":""How many times does this dependency appear.""}]}]" maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.search.FindMinimumJUnitVersion,Find minimum JUnit version,"A recipe to find the minimum version of JUnit dependencies. This recipe is designed to return the minimum version of JUnit in a project. It will search for JUnit 4 and JUnit 5 dependencies in the project. If both versions are found, it will return the minimum version of JUnit 4. @@ -32,3 +34,4 @@ If a minimumVersion is provided, the recipe will search to see if the minimum ve For example: if the minimumVersion is 4, and the project has JUnit 4.12 and JUnit 5.7, the recipe will return JUnit 4.12. If the project has only JUnit 5.7, the recipe will return JUnit 5.7. Another example: if the minimumVersion is 5, and the project has JUnit 4.12 and JUnit 5.7, the recipe will not return any results.",1,Search,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""minimumVersion"",""type"":""String"",""displayName"":""Version"",""description"":""Determine if the provided version is the minimum JUnit version. If both JUnit 4 and JUnit 5 are present, the minimum version is JUnit 4. If only one version is present, that version is the minimum version."",""example"":""4"",""valid"":[""4"",""5""]}]","[{""name"":""org.openrewrite.maven.table.DependenciesInUse"",""displayName"":""Dependencies in use"",""instanceName"":""Dependencies in use"",""description"":""Direct and transitive dependencies in use."",""columns"":[{""name"":""projectName"",""type"":""String"",""displayName"":""Project name"",""description"":""The name of the project that contains the dependency.""},{""name"":""sourceSet"",""type"":""String"",""displayName"":""Source set"",""description"":""The source set that contains the dependency.""},{""name"":""groupId"",""type"":""String"",""displayName"":""Group"",""description"":""The first part of a dependency coordinate `com.google.guava:guava:VERSION`.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact"",""description"":""The second part of a dependency coordinate `com.google.guava:guava:VERSION`.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The resolved version.""},{""name"":""datedSnapshotVersion"",""type"":""String"",""displayName"":""Dated snapshot version"",""description"":""The resolved dated snapshot version or `null` if this dependency is not a snapshot.""},{""name"":""scope"",""type"":""String"",""displayName"":""Scope"",""description"":""Dependency scope. This will be `compile` if the dependency is direct and a scope is not explicitly specified in the POM.""},{""name"":""count"",""type"":""Integer"",""displayName"":""Count"",""description"":""How many times does this dependency appear.""}]}]" maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.search.ModuleHasDependency,Module has dependency,"Searches for both Gradle and Maven modules that have a dependency matching the specified groupId and artifactId. Places a `SearchResult` marker on all sources within a module with a matching dependency. This recipe is intended to be used as a precondition for other recipes. For example this could be used to limit the application of a spring boot migration to only projects that use spring-boot-starter, limiting unnecessary upgrading. If the search result you want is instead just the build.gradle(.kts) or pom.xml file applying the plugin, use the `FindDependency` recipe instead.",1,Search,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupIdPattern"",""type"":""String"",""displayName"":""Group pattern"",""description"":""Group glob pattern used to match dependencies."",""example"":""com.fasterxml.jackson.module"",""required"":true},{""name"":""artifactIdPattern"",""type"":""String"",""displayName"":""Artifact pattern"",""description"":""Artifact glob pattern used to match dependencies."",""example"":""jackson-module-*"",""required"":true},{""name"":""scope"",""type"":""String"",""displayName"":""Scope"",""description"":""Match dependencies with the specified scope. All scopes are searched by default."",""example"":""compile"",""valid"":[""compile"",""test"",""runtime"",""provided"",""system""]},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""Match only dependencies with the specified version. Node-style [version selectors](https://docs.openrewrite.org/reference/dependency-version-selectors) may be used.All versions are searched by default."",""example"":""1.x""},{""name"":""invertMarking"",""type"":""Boolean"",""displayName"":""Invert marking"",""description"":""If `true`, will invert the check for whether to mark a file. Defaults to `false`.""}]", +maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.search.RepositoryHasDependency,Repository has dependency,"Searches for both Gradle and Maven modules that have a dependency matching the specified groupId and artifactId. Places a `SearchResult` marker on all sources within a repository with a matching dependency. This recipe is intended to be used as a precondition for other recipes. For example this could be used to limit the application of a spring boot migration to only projects that use a springframework dependency, limiting unnecessary upgrading. If the search result you want is instead just the build.gradle(.kts) or pom.xml file applying the plugin, use the `FindDependency` recipe instead.",1,Search,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupIdPattern"",""type"":""String"",""displayName"":""Group pattern"",""description"":""Group glob pattern used to match dependencies."",""example"":""com.fasterxml.jackson.module"",""required"":true},{""name"":""artifactIdPattern"",""type"":""String"",""displayName"":""Artifact pattern"",""description"":""Artifact glob pattern used to match dependencies."",""example"":""jackson-module-*"",""required"":true},{""name"":""scope"",""type"":""String"",""displayName"":""Scope"",""description"":""Match dependencies with the specified scope. All scopes are searched by default."",""example"":""compile"",""valid"":[""compile"",""test"",""runtime"",""provided"",""system""]},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""Match only dependencies with the specified version. Node-style [version selectors](https://docs.openrewrite.org/reference/dependency-version-selectors) may be used.All versions are searched by default."",""example"":""1.x""}]", diff --git a/src/test/java/org/openrewrite/java/dependencies/RepositoryLatencyDiagnosticTest.java b/src/test/java/org/openrewrite/java/dependencies/RepositoryLatencyDiagnosticTest.java new file mode 100644 index 00000000..cfc579cd --- /dev/null +++ b/src/test/java/org/openrewrite/java/dependencies/RepositoryLatencyDiagnosticTest.java @@ -0,0 +1,207 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed 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 + *

+ * https://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.openrewrite.java.dependencies; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.HttpSenderExecutionContextView; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.Parser; +import org.openrewrite.ipc.http.HttpSender; +import org.openrewrite.java.dependencies.table.RepositoryResponseLatency; +import org.openrewrite.maven.MavenExecutionContextView; +import org.openrewrite.maven.MavenSettings; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import java.io.ByteArrayInputStream; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openrewrite.maven.Assertions.pomXml; + +class RepositoryLatencyDiagnosticTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + // Keep the request count low so tests, which hit the real network, stay quick. + spec.recipe(new RepositoryLatencyDiagnostic(null, null, 2)); + } + + // CI may inject ~/.m2/settings.xml with `*` to route Maven through a cache, which would + // rewrite the pom-declared repositories. Force empty settings so the recipe sees the declared repositories. + private static MavenExecutionContextView emptySettingsContext() { + MavenExecutionContextView ctx = MavenExecutionContextView.view(new InMemoryExecutionContext()); + MavenSettings emptySettings = MavenSettings.parse(new Parser.Input(Path.of("settings.xml"), () -> new ByteArrayInputStream( + //language=xml + """ + + """.getBytes())), ctx); + ctx.setMavenSettings(emptySettings); + return ctx; + } + + @DocumentExample + @Test + void mavenCentralResponds() { + rewriteRun( + spec -> spec + .dataTable(RepositoryResponseLatency.Row.class, rows -> + assertThat(rows).anySatisfy(row -> { + assertThat(row.getRepositoryUri()).isEqualTo("https://repo.maven.apache.org/maven2"); + assertThat(row.getMetadataUri()) + .isEqualTo("https://repo.maven.apache.org/maven2/com/fasterxml/jackson/core/jackson-core/maven-metadata.xml"); + assertThat(row.getGroupArtifact()).isEqualTo("com.fasterxml.jackson.core:jackson-core"); + assertThat(row.getStatus1()).isEqualTo("200"); + assertThat(row.getHttpResponseCount()).isGreaterThanOrEqualTo(1); + assertThat(row.getConnectionFailureCount()).isZero(); + assertThat(row.getColdStartMs()).isNotNull(); + assertThat(row.getLatencyDecile5Ms()).isNotNull(); + // Fixed 10-column schema padded with empty statuses when fewer requests are made. + assertThat(row.getStatus3()).isEmpty(); + }) + ) + .executionContext(emptySettingsContext()), + //language=xml + pomXml( + """ + + com.example + test + 0.1.0 + + """ + ) + ); + } + + @Test + void unreachableRepositoryIsClassified() { + rewriteRun( + spec -> spec + .dataTable(RepositoryResponseLatency.Row.class, rows -> + assertThat(rows) + .filteredOn(row -> "https://nonexistent.moderne.io/maven2".equals(row.getRepositoryUri())) + .singleElement() + .satisfies(row -> { + assertThat(row.getHttpResponseCount()).isZero(); + assertThat(row.getConnectionFailureCount()).isEqualTo(2); + assertThat(row.getStatus1()).isEqualTo("UnknownHostException"); + assertThat(row.getLatencyDecile5Ms()).isNull(); + assertThat(row.getProbableBottleneck()).isEqualTo("UNREACHABLE"); + assertThat(row.getNotes()).contains("UnknownHostException"); + }) + ) + .executionContext(emptySettingsContext()), + //language=xml + pomXml( + """ + + com.example + test + 0.1.0 + + + nonexistent + https://nonexistent.moderne.io/maven2 + + + + """ + ) + ); + } + + @Test + void readsServerTimingHeader() { + // A stub HttpSender lets us assert the Server-Timing parsing deterministically: the real gateway/connector + // only emit the header in a deployed tunnel, and Maven Central never does. + HttpSender stub = request -> new HttpSender.Response(200, new ByteArrayInputStream("".getBytes()), + Map.of( + "Server-Timing", List.of("repo;dur=250, rsocket;dur=40"), + "Server", List.of("StubServer")), + () -> { + }); + + rewriteRun( + spec -> { + MavenExecutionContextView ctx = emptySettingsContext(); + HttpSenderExecutionContextView.view(ctx).setHttpSender(stub); + spec + .dataTable(RepositoryResponseLatency.Row.class, rows -> + assertThat(rows) + .filteredOn(row -> "https://stub.moderne.io/maven2".equals(row.getRepositoryUri())) + .singleElement() + .satisfies(row -> { + assertThat(row.getStatus1()).isEqualTo("200"); + assertThat(row.getHttpResponseCount()).isEqualTo(2); + assertThat(row.getServerTimingRepositoryMs()).isEqualTo(250L); + assertThat(row.getServerTimingRsocketMs()).isEqualTo(40L); + assertThat(row.getUpstreamServer()).contains("StubServer"); + assertThat(row.getColdStartMs()).isNotNull(); + })) + .executionContext(ctx); + }, + //language=xml + pomXml( + """ + + com.example + test + 0.1.0 + + + stub + https://stub.moderne.io/maven2 + + + + """ + ) + ); + } + + @Test + void configurableArtifact() { + rewriteRun( + spec -> spec + .recipe(new RepositoryLatencyDiagnostic("com.google.guava", "guava", 2)) + .dataTable(RepositoryResponseLatency.Row.class, rows -> + assertThat(rows).anySatisfy(row -> { + assertThat(row.getRepositoryUri()).isEqualTo("https://repo.maven.apache.org/maven2"); + assertThat(row.getGroupArtifact()).isEqualTo("com.google.guava:guava"); + assertThat(row.getMetadataUri()) + .isEqualTo("https://repo.maven.apache.org/maven2/com/google/guava/guava/maven-metadata.xml"); + }) + ) + .executionContext(emptySettingsContext()), + //language=xml + pomXml( + """ + + com.example + test + 0.1.0 + + """ + ) + ); + } +}