This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new fbf33fc74595 CAMEL-24761: camel-jbang - cache the Camel to Quarkus
platform mapping (#27115)
fbf33fc74595 is described below
commit fbf33fc74595261d9be7784d1a8526dfd9eafb90
Author: Brijesh Thakkar <[email protected]>
AuthorDate: Mon Oct 5 14:10:49 2026 +0530
CAMEL-24761: camel-jbang - cache the Camel to Quarkus platform mapping
(#27115)
* CAMEL-24761: camel-jbang - cache the Camel to Quarkus platform mapping
* CAMEL-24761: camel-jbang - add TTL and atomic writes to Quarkus platform
mapping cache
* CAMEL-24761: camel-jbang - isolate registry caches by port, move upgrade
note to 4.23
The Quarkus Extension Registry cache directory was keyed on the host only,
so
two registries on the same host but different ports shared both the cached
registry response and the platform mapping, and switching registries with
--quarkus-ext-registry or camel.jbang.quarkusExtensionRegistryBaseUri could
serve the other registry's platform. Include the port in the directory name;
the default registry (no explicit port) keeps its directory.
Move the caching note from the 4.21 upgrade guide to the camel-jbang section
of the 4.23 guide, where the behaviour ships.
* Merge branch 'main' into CAMEL-24761-quarkus-platform-cache
* CAMEL-24761: camel-jbang - separate the platform cache note from the next
paragraph in the 4.23 upgrade guide
After the merge with main, the Quarkus platform cache note ran straight into
the camel validate source paragraph that landed at the same spot, so
AsciiDoc
rendered the two notes as one paragraph. Add the missing blank line.
* Merge branch 'main' into CAMEL-24761-quarkus-platform-cache
* CAMEL-24761: camel-jbang - document the platform cache on the validation
page
The docs restructure in #27301 moved the Validate Plugin section from
camel-jbang-devtools.adoc to camel-jbang-validation.adoc, so the updated
caching sentence was lost in the merge with main. Re-apply it there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 7 +
.../modules/ROOT/pages/camel-jbang-validation.adoc | 6 +-
.../camel/dsl/jbang/core/common/QuarkusHelper.java | 139 ++++++-
.../core/commands/QuarkusPlatformMixinTest.java | 112 ++++++
.../dsl/jbang/core/common/QuarkusHelperTest.java | 398 ++++++++++++++++++++-
5 files changed, 657 insertions(+), 5 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index d90d50f5d3e9..d65aa7a14049 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -1774,6 +1774,13 @@ named as written.
`--dep=camel-groovy`; before, the file was silently ignored unless the
dependency was added. The dependency is
also written to the run settings, so `camel export` includes it.
+With `--runtime=quarkus`, the Quarkus Platform resolved for a released Camel
version is now cached for 7 days
+under `~/.camel/quarkus-extension-registries`, next to the Quarkus Extension
Registry response, which is still
+fetched at most once a day. Only a platform that carries exactly the requested
Camel version is cached; a
+`-SNAPSHOT` Camel version is never cached. `--fresh` clears both caches. A
registry with an explicit port in
+`--quarkus-ext-registry` (or `camel.jbang.quarkusExtensionRegistryBaseUri`)
now has its own cache directory, so
+two registries on the same host no longer share their cached responses.
+
`camel validate source`, and the `camel_validate_source` and
`camel_write_file` MCP tools, now check the routes of
Java and XML DSL files as they check YAML routes: the endpoint URIs against
the catalog (also those built with the
endpoint DSL or from a constant), simple expressions, and a `to` whose URI
holds `+${...}+` and should be a `toD`.
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-validation.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-validation.adoc
index b861f804b5d3..3503d080929f 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-validation.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-validation.adoc
@@ -129,8 +129,10 @@ Validation error detected (errors:1)
The catalog of that version is downloaded as for `camel catalog`, and the
schema is read from the
`camel-yaml-dsl` jar of that version (`--repos` and `--download=false` apply
to both). With `--runtime=quarkus`
-the Quarkus platform is resolved from the Camel version through the Quarkus
extension registry (cached for a
-day under `~/.camel`), or given with `--quarkus-version`; a component of the
route that has no Camel Quarkus
+the Quarkus platform is resolved from the Camel version through the Quarkus
extension registry (the response is
+cached for a day under `~/.camel`, and the platform resolved for a released
Camel version that the platform
+carries exactly is cached for 7 days; a `-SNAPSHOT` version is never cached,
and `--fresh` clears both), or given
+with `--quarkus-version`; a component of the route that has no Camel Quarkus
extension, or no Camel Spring Boot starter with `--runtime=spring-boot`, is an
error. `--canonical` needs Camel
4.22 or later, the first version with the canonical schema.
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/QuarkusHelper.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/QuarkusHelper.java
index 9e38839d0665..f13db66d9450 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/QuarkusHelper.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/QuarkusHelper.java
@@ -26,10 +26,13 @@ import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
+import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.FileTime;
+import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
@@ -77,6 +80,14 @@ public final class QuarkusHelper {
public static final String QUARKUS_PLATFORM_URL_PROPERTY =
"camel.jbang.quarkus.platform.url";
+ private static final String PLATFORM_MAPPING_FILE =
"platform-mapping.json";
+
+ /**
+ * How long a remembered Camel to Quarkus platform mapping is trusted. The
registry may later publish a newer
+ * platform for the same Camel version (a respin), so the mapping is not
kept forever.
+ */
+ private static final Duration PLATFORM_MAPPING_TTL = Duration.ofDays(7);
+
private QuarkusHelper() {
}
@@ -127,6 +138,9 @@ public final class QuarkusHelper {
* <p>
* This is used by export/run commands to query the registry for the
correct platform BOM version instead of using
* the build-time constant. The registry may have a newer compatible
version.
+ * <p>
+ * A platform whose Camel version is exactly the requested released {@code
camelVersion} is remembered next to the
+ * cached registry response and reused without asking the registry again
for 7 days. {@code fresh} drops it.
*
* @param camelVersion if specified, the value of
{@code --camel-version} CLI parameter or the
* Camel version of the currently
running camel-jbang. Must not be
@@ -142,19 +156,138 @@ public final class QuarkusHelper {
boolean download,
String quarkusExtensionRegistryBaseUri,
boolean fresh) {
+ return findQuarkusPlatformBom(
+ camelVersion, mavenResolver, download,
quarkusExtensionRegistryBaseUri, fresh, registriesDir(),
+ Clock.systemUTC());
+ }
+
+ static QuarkusPlatformBom findQuarkusPlatformBom(
+ String camelVersion,
+ Function<MavenGav, MavenArtifact> mavenResolver,
+ boolean download,
+ String quarkusExtensionRegistryBaseUri,
+ boolean fresh,
+ Path registriesDir,
+ Clock clock) {
if (camelVersion == null) {
camelVersion = RuntimeType.main.version();
}
+ final boolean snapshot = camelVersion.endsWith("-SNAPSHOT");
+
+ final Path mappingFile =
platformMappingFile(quarkusExtensionRegistryBaseUri, registriesDir);
+ if (mappingFile != null) {
+ if (fresh) {
+ if (download) {
+ try {
+ Files.deleteIfExists(mappingFile);
+ } catch (IOException e) {
+ // the mapping is only a cache, the registry is asked
below
+ }
+ }
+ } else if (!snapshot) {
+ QuarkusPlatformBom cached
+ = readPlatformMapping(mappingFile, camelVersion,
quarkusExtensionRegistryBaseUri, clock);
+ if (cached != null) {
+ return cached;
+ }
+ }
+ }
- JsonArray streams =
fetchPlatformStreams(quarkusExtensionRegistryBaseUri, download, fresh,
registriesDir());
+ JsonArray streams =
fetchPlatformStreams(quarkusExtensionRegistryBaseUri, download, fresh,
registriesDir);
if (streams == null || streams.isEmpty()) {
return null;
}
Optional<QuarkusPlatformBom> resolved
= findPlatformBom(streams, new MajorMinor(camelVersion),
mavenResolver, quarkusExtensionRegistryBaseUri);
+ // only a platform with exactly the requested released Camel version
is final; a fallback may be replaced
+ if (mappingFile != null && !snapshot && resolved.isPresent() &&
camelVersion.equals(resolved.get().camelVersion())) {
+ storePlatformMapping(mappingFile, camelVersion, resolved.get(),
clock);
+ }
return resolved.orElse(null);
}
+ /**
+ * @return the file remembering the Camel version to platform mapping of
the given registry, or {@code null} for a
+ * {@code file://} registry, which is not cached, or an invalid
URI, which {@link #fetchPlatformStreams}
+ * reports
+ */
+ private static Path platformMappingFile(String
quarkusExtensionRegistryBaseUri, Path registriesDir) {
+ try {
+ final URI uri = new URI(quarkusExtensionRegistryBaseUri +
"/client/platforms/all");
+ if ("file".equals(uri.getScheme())) {
+ return null;
+ }
+ return cacheFile(uri,
registriesDir).resolveSibling(PLATFORM_MAPPING_FILE);
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ }
+
+ /**
+ * @return the platform remembered for the given Camel version, or {@code
null} if there is none or the entry is
+ * unusable: incomplete, without a timestamp, stamped in the
future or older than
+ * {@link #PLATFORM_MAPPING_TTL}
+ */
+ private static QuarkusPlatformBom readPlatformMapping(
+ Path mappingFile, String camelVersion, String
quarkusExtensionRegistryBaseUri, Clock clock) {
+ if (readPlatformMappings(mappingFile).get(camelVersion) instanceof
JsonObject entry
+ && entry.get("groupId") instanceof String groupId &&
!groupId.isBlank()
+ && entry.get("version") instanceof String version &&
!version.isBlank()
+ && entry.get("resolvedAt") instanceof Number resolvedAt) {
+ final long age = clock.millis() - resolvedAt.longValue();
+ if (age >= 0 && age <= PLATFORM_MAPPING_TTL.toMillis()) {
+ return new QuarkusPlatformBom(groupId, version, camelVersion,
quarkusExtensionRegistryBaseUri);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Concurrent processes are last-writer-wins: the file is replaced
atomically, so it is never torn, and an entry
+ * lost to another writer is only a cache miss.
+ */
+ private static void storePlatformMapping(Path mappingFile, String
camelVersion, QuarkusPlatformBom bom, Clock clock) {
+ JsonObject mappings = readPlatformMappings(mappingFile);
+ JsonObject entry = new JsonObject();
+ entry.put("groupId", bom.groupId());
+ entry.put("version", bom.version());
+ entry.put("resolvedAt", clock.millis());
+ mappings.put(camelVersion, entry);
+ Path tmp = null;
+ try {
+ Path dir = mappingFile.getParent();
+ Files.createDirectories(dir);
+ tmp = Files.createTempFile(dir, PLATFORM_MAPPING_FILE, ".tmp");
+ Files.writeString(tmp, Jsoner.serialize(mappings));
+ try {
+ Files.move(tmp, mappingFile, StandardCopyOption.ATOMIC_MOVE);
+ } catch (AtomicMoveNotSupportedException e) {
+ Files.move(tmp, mappingFile,
StandardCopyOption.REPLACE_EXISTING);
+ }
+ } catch (IOException e) {
+ // the mapping is only a cache, the next run asks the registry
again
+ } finally {
+ if (tmp != null) {
+ try {
+ Files.deleteIfExists(tmp);
+ } catch (IOException e) {
+ // nothing more can be done for a leftover temp file
+ }
+ }
+ }
+ }
+
+ private static JsonObject readPlatformMappings(Path mappingFile) {
+ try {
+ if (Jsoner.deserialize(Files.readString(mappingFile)) instanceof
JsonObject mappings) {
+ return mappings;
+ }
+ } catch (IOException | DeserializationException e) {
+ // a missing, unreadable or corrupt mapping is a cache miss
+ }
+ return new JsonObject();
+ }
+
public static String resolveCamelVersionFromQuarkusCamelBom(
MavenGav quarkusCamelBom, Function<MavenGav, MavenArtifact>
mavenResolver) {
Path file = mavenResolver.apply(quarkusCamelBom).getFile().toPath();
@@ -260,7 +393,9 @@ public final class QuarkusHelper {
if (relPath != null && relPath.startsWith("/")) {
relPath = relPath.substring(1);
}
- final Path cacheFile =
registriesDir.resolve(uri.getHost()).resolve(relPath + ".json").normalize();
+ // the port is part of the registry: two registries on one host must
not share their caches
+ final String registryDir = uri.getPort() < 0 ? uri.getHost() :
uri.getHost() + "_" + uri.getPort();
+ final Path cacheFile =
registriesDir.resolve(registryDir).resolve(relPath + ".json").normalize();
if (!cacheFile.startsWith(registriesDir)) {
throw new IllegalStateException(
"Could not create a safe cache directory for " + uri + ":
" + cacheFile + " is not under "
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/QuarkusPlatformMixinTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/QuarkusPlatformMixinTest.java
index d53b047b433d..f7a060e859b9 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/QuarkusPlatformMixinTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/QuarkusPlatformMixinTest.java
@@ -16,17 +16,53 @@
*/
package org.apache.camel.dsl.jbang.core.commands;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.Properties;
+import java.util.function.Function;
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
import org.apache.camel.dsl.jbang.core.common.QuarkusHelper;
+import org.apache.camel.dsl.jbang.core.common.QuarkusHelper.QuarkusPlatformBom;
import org.apache.camel.dsl.jbang.core.common.RuntimeType;
+import org.apache.camel.tooling.maven.MavenArtifact;
+import org.apache.camel.tooling.maven.MavenGav;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
+import picocli.CommandLine;
import static
org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.QUARKUS_EXTENSION_REGISTRY_BASE_URI;
import static org.assertj.core.api.Assertions.assertThat;
class QuarkusPlatformMixinTest {
+ private static final String PLATFORMS_JSON =
"target/test-classes/QuarkusHelperTest/quarkus-registry-client-platforms.json";
+
+ /** Registry A has platform 3.27.3.1 for Camel 4.14.5 */
+ @RegisterExtension
+ static WireMockExtension registryA = WireMockExtension.newInstance()
+ .options(WireMockConfiguration.wireMockConfig().dynamicPort())
+ .build();
+
+ /** Registry B, on the same host but another port, has a respin 3.27.4 for
the same Camel version */
+ @RegisterExtension
+ static WireMockExtension registryB = WireMockExtension.newInstance()
+ .options(WireMockConfiguration.wireMockConfig().dynamicPort())
+ .build();
+
+ @TempDir
+ Path home;
+
+ private String originalHome;
+
private static final QuarkusPlatformMixinSpec DEFAULT_FALLBACK = new
QuarkusPlatformMixinSpec() {
@Override
public String quarkusGroupId() {
@@ -118,4 +154,80 @@ class QuarkusPlatformMixinTest {
assertThat(afterPhase2.quarkusExtensionRegistryBaseUri()).isEqualTo("https://from-sys-prop.example.com");
}
+ @BeforeEach
+ void useTempHome() throws IOException {
+ originalHome = CommandLineHelper.getHomeDir().toString();
+ CommandLineHelper.useHomeDir(home.toString());
+ String platforms = Files.readString(Path.of(PLATFORMS_JSON));
+ stub(registryA, platforms);
+ stub(registryB, platforms.replace("3.27.3.1", "3.27.4"));
+ }
+
+ @AfterEach
+ void restoreHome() {
+ CommandLineHelper.useHomeDir(originalHome);
+ }
+
+ private static void stub(WireMockExtension registry, String body) {
+ registry.resetAll();
+
registry.stubFor(WireMock.get(WireMock.urlEqualTo("/client/platforms/all"))
+ .willReturn(WireMock.ok().withHeader("Content-Type",
"application/json").withBody(body)));
+ }
+
+ /** The BOM fixtures of QuarkusHelperTest; the 3.27.4 respin pins the same
Camel version as 3.27.3.1 */
+ private static final Function<MavenGav, MavenArtifact> RESOLVER = gav -> {
+ String version = "3.27.4".equals(gav.getVersion()) ? "3.27.3.1" :
gav.getVersion();
+ Path pom =
Path.of("target/test-classes/QuarkusHelperTest/quarkus-camel-bom-" + version +
".pom.xml");
+ if (!Files.isRegularFile(pom)) {
+ throw new UncheckedIOException(new IOException("No fixture for " +
gav));
+ }
+ return new MavenArtifact(gav, pom.toFile());
+ };
+
+ private static QuarkusPlatformBom resolveWithFlag(WireMockExtension
registry) {
+ QuarkusPlatformMixin mixin = new QuarkusPlatformMixin();
+ CommandLine.populateCommand(mixin, "--quarkus-ext-registry=" +
registry.baseUrl());
+ return mixin.resolve("4.14.5", RESOLVER, true, false);
+ }
+
+ private static QuarkusPlatformBom resolveWithProperty(WireMockExtension
registry) {
+ Properties props = new Properties();
+ props.setProperty(QUARKUS_EXTENSION_REGISTRY_BASE_URI,
registry.baseUrl());
+ return QuarkusPlatformMixin.of(props,
DEFAULT_FALLBACK).resolve("4.14.5", RESOLVER, true, false);
+ }
+
+ private static void assertResolvedFrom(QuarkusPlatformBom bom,
WireMockExtension registry, String platformVersion) {
+ assertThat(bom.version()).isEqualTo(platformVersion);
+ assertThat(bom.camelVersion()).isEqualTo("4.14.5");
+
assertThat(bom.quarkusExtensionRegistryBaseUri()).isEqualTo(registry.baseUrl());
+ }
+
+ private static void assertAskedOnce(WireMockExtension registry) {
+ registry.verify(1,
WireMock.getRequestedFor(WireMock.urlEqualTo("/client/platforms/all")));
+ }
+
+ @Test
+ void alternatingRegistryFlagDoesNotServeTheOtherRegistrysCache() {
+ assertResolvedFrom(resolveWithFlag(registryA), registryA, "3.27.3.1");
+ assertResolvedFrom(resolveWithFlag(registryB), registryB, "3.27.4");
+ // back and forth again: each registry answers from its own cache
+ assertResolvedFrom(resolveWithFlag(registryA), registryA, "3.27.3.1");
+ assertResolvedFrom(resolveWithFlag(registryB), registryB, "3.27.4");
+
+ assertAskedOnce(registryA);
+ assertAskedOnce(registryB);
+ }
+
+ @Test
+ void alternatingRegistryPropertyDoesNotServeTheOtherRegistrysCache() {
+ assertResolvedFrom(resolveWithProperty(registryA), registryA,
"3.27.3.1");
+ assertResolvedFrom(resolveWithProperty(registryB), registryB,
"3.27.4");
+ // the flag and the property name the same registry, so they share its
cache
+ assertResolvedFrom(resolveWithFlag(registryA), registryA, "3.27.3.1");
+ assertResolvedFrom(resolveWithProperty(registryB), registryB,
"3.27.4");
+
+ assertAskedOnce(registryA);
+ assertAskedOnce(registryB);
+ }
+
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/QuarkusHelperTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/QuarkusHelperTest.java
index 7a0e2eeffa81..8f94487cf020 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/QuarkusHelperTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/QuarkusHelperTest.java
@@ -19,9 +19,16 @@ package org.apache.camel.dsl.jbang.core.common;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneOffset;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.UUID;
+import java.util.function.Function;
import java.util.stream.Stream;
import com.github.tomakehurst.wiremock.client.WireMock;
@@ -159,7 +166,7 @@ public class QuarkusHelperTest {
.withBody(allPlatformsJson)));
JsonArray arr = QuarkusHelper.fetchPlatformStreams(wireMock.baseUrl(),
true, false, registriesDir);
-
Assertions.assertThat(registriesDir.resolve("localhost/client/platforms/all.json")).hasContent(allPlatformsJson);
+
Assertions.assertThat(registryDir(registriesDir).resolve("client/platforms/all.json")).hasContent(allPlatformsJson);
wireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/client/platforms/all"))
.willReturn(WireMock.forbidden()));
@@ -167,4 +174,393 @@ public class QuarkusHelperTest {
JsonArray arr2 =
QuarkusHelper.fetchPlatformStreams(wireMock.baseUrl(), true, false,
registriesDir);
Assertions.assertThat(arr2).isEqualTo(arr);
}
+
+ private static final String CAMEL_4_14_5_PLATFORM = "3.27.3.1";
+
+ private static Path newRegistriesDir() throws IOException {
+ Path registriesDir
+ = Path.of("target/" + QuarkusHelperTest.class.getSimpleName()
+ "-" + UUID.randomUUID() + "/registries");
+ Files.createDirectories(registriesDir);
+ return registriesDir;
+ }
+
+ /** The cache directory of the WireMock registry: its host and, as it is
not a default one, its port */
+ private static Path registryDir(Path registriesDir) {
+ return registriesDir.resolve("localhost_" + wireMock.getPort());
+ }
+
+ private static Path mappingFile(Path registriesDir) {
+ return
registryDir(registriesDir).resolve("client/platforms/platform-mapping.json");
+ }
+
+ private static Path registryCacheFile(Path registriesDir) {
+ return registryDir(registriesDir).resolve("client/platforms/all.json");
+ }
+
+ /** Serves the registry document matching the BOM fixtures of this test
and forgets earlier requests. */
+ private static void stubRegistry() throws IOException {
+ wireMock.resetAll();
+
wireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/client/platforms/all"))
+ .willReturn(WireMock.ok()
+ .withHeader("Content-Type", "application/json")
+ .withBody(Files.readString(
+
Path.of("target/test-classes/QuarkusHelperTest/quarkus-registry-client-platforms.json")))));
+ }
+
+ private static void verifyRegistryRequests(int count) {
+ wireMock.verify(count,
WireMock.getRequestedFor(WireMock.urlEqualTo("/client/platforms/all")));
+ }
+
+ private static QuarkusPlatformBom find(
+ String camelVersion, boolean download, boolean fresh, Path
registriesDir) {
+ return find(camelVersion, QuarkusHelperTest::resolve, download, fresh,
registriesDir);
+ }
+
+ private static QuarkusPlatformBom find(
+ String camelVersion, Function<MavenGav, MavenArtifact>
mavenResolver, boolean download, boolean fresh,
+ Path registriesDir) {
+ return find(camelVersion, mavenResolver, download, fresh,
registriesDir, Clock.systemUTC());
+ }
+
+ private static QuarkusPlatformBom find(
+ String camelVersion, Function<MavenGav, MavenArtifact>
mavenResolver, boolean download, boolean fresh,
+ Path registriesDir, Clock clock) {
+ return QuarkusHelper.findQuarkusPlatformBom(
+ camelVersion, mavenResolver, download, wireMock.baseUrl(),
fresh, registriesDir, clock);
+ }
+
+ private static Clock clockAt(Instant instant) {
+ return Clock.fixed(instant, ZoneOffset.UTC);
+ }
+
+ private static QuarkusPlatformBom find(String camelVersion, Path
registriesDir, Clock clock) {
+ return find(camelVersion, QuarkusHelperTest::resolve, true, false,
registriesDir, clock);
+ }
+
+ private static long resolvedAt(Path mappingFile, String camelVersion)
throws Exception {
+ JsonObject mappings = (JsonObject)
Jsoner.deserialize(Files.readString(mappingFile));
+ return ((Number)
mappings.getMap(camelVersion).get("resolvedAt")).longValue();
+ }
+
+ private static void backdate(Path file) throws IOException {
+ Files.setLastModifiedTime(file,
FileTime.from(Instant.now().minus(Duration.ofDays(2))));
+ }
+
+ @Test
+ void releasedVersionIsCachedOnFirstResolution() throws Exception {
+ stubRegistry();
+ Path registriesDir = newRegistriesDir();
+
+ Instant start = Instant.parse("2026-01-01T00:00:00Z");
+ QuarkusPlatformBom bom = find("4.14.5", registriesDir, clockAt(start));
+
+ Assertions.assertThat(bom).isEqualTo(
+ new QuarkusPlatformBom("io.quarkus.platform",
CAMEL_4_14_5_PLATFORM, "4.14.5", wireMock.baseUrl()));
+ JsonObject mappings = (JsonObject)
Jsoner.deserialize(Files.readString(mappingFile(registriesDir)));
+ Map<String, Object> entry = mappings.getMap("4.14.5");
+ Assertions.assertThat(entry)
+ .containsEntry("groupId", "io.quarkus.platform")
+ .containsEntry("version", CAMEL_4_14_5_PLATFORM)
+ .hasSize(3);
+ Assertions.assertThat(resolvedAt(mappingFile(registriesDir),
"4.14.5")).isEqualTo(start.toEpochMilli());
+ }
+
+ @Test
+ void cachedMappingSkipsRegistryAndMaven() throws Exception {
+ stubRegistry();
+ Path registriesDir = newRegistriesDir();
+ QuarkusPlatformBom first = find("4.14.5", true, false, registriesDir);
+
+ Files.delete(registryCacheFile(registriesDir));
+ wireMock.resetRequests();
+ QuarkusPlatformBom second = find("4.14.5", platform -> {
+ throw new IllegalStateException("Maven should not be asked for " +
platform);
+ }, true, false, registriesDir);
+
+ Assertions.assertThat(second).isEqualTo(first);
+ verifyRegistryRequests(0);
+ }
+
+ @Test
+ void cachedMappingIgnoresRegistryAge() throws Exception {
+ stubRegistry();
+ Path registriesDir = newRegistriesDir();
+ QuarkusPlatformBom first = find("4.14.5", true, false, registriesDir);
+
+ backdate(registryCacheFile(registriesDir));
+ QuarkusPlatformBom second = find("4.14.5", true, false, registriesDir);
+
+ Assertions.assertThat(second).isEqualTo(first);
+ verifyRegistryRequests(1);
+ }
+
+ @Test
+ void nonExactMatchIsNotCached() throws Exception {
+ stubRegistry();
+ Path registriesDir = newRegistriesDir();
+
+ // BOM 3.27.3.1 has Camel 4.14.5, so it is only a fallback for 4.14.0
+ QuarkusPlatformBom bom = find("4.14.0", true, false, registriesDir);
+ Assertions.assertThat(bom.version()).isEqualTo(CAMEL_4_14_5_PLATFORM);
+ Assertions.assertThat(bom.camelVersion()).isEqualTo("4.14.5");
+ Assertions.assertThat(mappingFile(registriesDir)).doesNotExist();
+
+ backdate(registryCacheFile(registriesDir));
+ find("4.14.0", true, false, registriesDir);
+ verifyRegistryRequests(2);
+ }
+
+ @Test
+ void snapshotIsNeverCachedNorLookedUp() throws Exception {
+ stubRegistry();
+ Path registriesDir = newRegistriesDir();
+ Path mappingFile = mappingFile(registriesDir);
+ Files.createDirectories(mappingFile.getParent());
+ String planted =
"{\"4.14.5-SNAPSHOT\":{\"groupId\":\"planted\",\"version\":\"0\",\"resolvedAt\":"
+ + System.currentTimeMillis() + "}}";
+ Files.writeString(mappingFile, planted);
+
+ QuarkusPlatformBom bom = find("4.14.5-SNAPSHOT", true, false,
registriesDir);
+
+ Assertions.assertThat(bom.version()).isEqualTo(CAMEL_4_14_5_PLATFORM);
+ Assertions.assertThat(mappingFile).hasContent(planted);
+ verifyRegistryRequests(1);
+ }
+
+ @Test
+ void freshDropsAndRebuildsMapping() throws Exception {
+ stubRegistry();
+ Path registriesDir = newRegistriesDir();
+ Path mappingFile = mappingFile(registriesDir);
+ Files.createDirectories(mappingFile.getParent());
+ Files.writeString(mappingFile,
"{\"4.14.5\":{\"groupId\":\"planted\",\"version\":\"0\",\"resolvedAt\":"
+ + System.currentTimeMillis() + "}}");
+
+ // the planted entry is fresh enough to be trusted, but --fresh does
not trust it: the registry is asked
+ QuarkusPlatformBom bom = find("4.14.5", true, true, registriesDir);
+ Assertions.assertThat(bom.groupId()).isEqualTo("io.quarkus.platform");
+ Assertions.assertThat(bom.version()).isEqualTo(CAMEL_4_14_5_PLATFORM);
+
Assertions.assertThat(mappingFile).content().contains(CAMEL_4_14_5_PLATFORM).doesNotContain("planted");
+ verifyRegistryRequests(1);
+
+ // a resolution that cannot be remembered leaves no mapping at all
+ find("4.14.0", true, true, registriesDir);
+ Assertions.assertThat(mappingFile).doesNotExist();
+ verifyRegistryRequests(2);
+ }
+
+ @Test
+ void freshWithoutDownloadStillContradicts() throws Exception {
+ stubRegistry();
+ Path registriesDir = newRegistriesDir();
+ find("4.14.5", true, false, registriesDir);
+ wireMock.resetRequests();
+
+ Assertions.assertThatThrownBy(() -> find("4.14.5", false, true,
registriesDir))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("contradict each other");
+ Assertions.assertThat(mappingFile(registriesDir)).exists();
+ verifyRegistryRequests(0);
+ }
+
+ @Test
+ void downloadFalseUsesMappingWithoutRegistryCache() throws Exception {
+ stubRegistry();
+ Path registriesDir = newRegistriesDir();
+ QuarkusPlatformBom first = find("4.14.5", true, false, registriesDir);
+
+ Files.delete(registryCacheFile(registriesDir));
+ wireMock.resetRequests();
+ QuarkusPlatformBom second = find("4.14.5", false, false,
registriesDir);
+
+ Assertions.assertThat(second).isEqualTo(first);
+ verifyRegistryRequests(0);
+ }
+
+ @Test
+ void downloadFalseFallsBackToOldRegistryCache() throws Exception {
+ stubRegistry();
+ Path registriesDir = newRegistriesDir();
+ QuarkusPlatformBom first = find("4.14.5", true, false, registriesDir);
+
+ Files.delete(mappingFile(registriesDir));
+ backdate(registryCacheFile(registriesDir));
+ QuarkusPlatformBom second = find("4.14.5", false, false,
registriesDir);
+
+ Assertions.assertThat(second).isEqualTo(first);
+ Assertions.assertThat(mappingFile(registriesDir)).exists();
+ verifyRegistryRequests(1);
+ }
+
+ @Test
+ void corruptMappingFallsBack() throws Exception {
+ stubRegistry();
+ Path registriesDir = newRegistriesDir();
+ QuarkusPlatformBom expected = find("4.14.5", true, false,
registriesDir);
+ Path mappingFile = mappingFile(registriesDir);
+
+ String now = String.valueOf(System.currentTimeMillis());
+ for (String corrupt : List.of(
+ "",
+ "not json {",
+ "[1, 2]",
+ "{\"4.14.5\":\"3.27.3.1\"}",
+ "{\"4.14.5\":{\"version\":\"3.27.3.1\",\"resolvedAt\":" + now
+ "}}",
+ "{\"4.14.5\":{\"groupId\":\"
\",\"version\":\"3.27.3.1\",\"resolvedAt\":" + now + "}}",
+
"{\"4.14.5\":{\"groupId\":\"io.quarkus.platform\",\"version\":7,\"resolvedAt\":"
+ now + "}}",
+
"{\"4.14.5\":{\"groupId\":\"io.quarkus.platform\",\"version\":\"3.27.3.1\",\"resolvedAt\":\"yesterday\"}}"))
{
+ Files.writeString(mappingFile, corrupt);
+
+ Assertions.assertThat(find("4.14.5", true, false,
registriesDir)).as(corrupt).isEqualTo(expected);
+ Assertions.assertThat(find("4.14.5", false, false,
registriesDir)).as(corrupt).isEqualTo(expected);
+ }
+ verifyRegistryRequests(1);
+ }
+
+ @Test
+ void unreadableMappingFallsBack() throws Exception {
+ stubRegistry();
+ Path registriesDir = newRegistriesDir();
+ // a directory can be neither read nor overwritten as the mapping file
+ Files.createDirectories(mappingFile(registriesDir));
+
+ QuarkusPlatformBom bom = find("4.14.5", true, false, registriesDir);
+
+ Assertions.assertThat(bom.version()).isEqualTo(CAMEL_4_14_5_PLATFORM);
+ Assertions.assertThat(mappingFile(registriesDir)).isDirectory();
+ assertNoTempFiles(registriesDir);
+ }
+
+ @Test
+ void fileRegistryIsNotCached() throws Exception {
+ Path registriesDir = newRegistriesDir();
+ Path registry = registriesDir.resolveSibling("file-registry");
+ Files.createDirectories(registry.resolve("client/platforms"));
+
Files.copy(Path.of("target/test-classes/QuarkusHelperTest/quarkus-registry-client-platforms.json"),
+ registry.resolve("client/platforms/all.json"));
+ String baseUri = registry.toUri().toString();
+ baseUri = baseUri.substring(0, baseUri.length() - 1);
+
+ QuarkusPlatformBom bom = QuarkusHelper.findQuarkusPlatformBom(
+ "4.14.5", QuarkusHelperTest::resolve, true, baseUri, false,
registriesDir, Clock.systemUTC());
+
+ Assertions.assertThat(bom.version()).isEqualTo(CAMEL_4_14_5_PLATFORM);
+ Assertions.assertThat(registriesDir).isEmptyDirectory();
+ }
+
+ private static void assertNoTempFiles(Path registriesDir) throws
IOException {
+ try (Stream<Path> files =
Files.list(mappingFile(registriesDir).getParent())) {
+ Assertions.assertThat(files.map(f ->
f.getFileName().toString())).noneMatch(name -> name.endsWith(".tmp"));
+ }
+ }
+
+ @Test
+ void entryWithinTtlIsAHit() throws Exception {
+ stubRegistry();
+ Path registriesDir = newRegistriesDir();
+ Instant start = Instant.parse("2026-01-01T00:00:00Z");
+ QuarkusPlatformBom first = find("4.14.5", registriesDir,
clockAt(start));
+ wireMock.resetRequests();
+
+ QuarkusPlatformBom second = find("4.14.5", platform -> {
+ throw new IllegalStateException("Maven should not be asked for " +
platform);
+ }, true, false, registriesDir,
clockAt(start.plus(Duration.ofDays(6))));
+
+ Assertions.assertThat(second).isEqualTo(first);
+ verifyRegistryRequests(0);
+ Assertions.assertThat(resolvedAt(mappingFile(registriesDir),
"4.14.5")).isEqualTo(start.toEpochMilli());
+ }
+
+ @Test
+ void entryPastTtlIsRefreshed() throws Exception {
+ stubRegistry();
+ Path registriesDir = newRegistriesDir();
+ Instant start = Instant.parse("2026-01-01T00:00:00Z");
+ QuarkusPlatformBom first = find("4.14.5", registriesDir,
clockAt(start));
+
+ // the registry answers from its own cache for a day, so age it to see
the mapping being asked again
+ backdate(registryCacheFile(registriesDir));
+ Instant later = start.plus(Duration.ofDays(8));
+ QuarkusPlatformBom second = find("4.14.5", registriesDir,
clockAt(later));
+
+ Assertions.assertThat(second).isEqualTo(first);
+ verifyRegistryRequests(2);
+ Assertions.assertThat(resolvedAt(mappingFile(registriesDir),
"4.14.5")).isEqualTo(later.toEpochMilli());
+ }
+
+ @Test
+ void respinIsPickedUpAfterTtl() throws Exception {
+ stubRegistry();
+ Path registriesDir = newRegistriesDir();
+ Instant start = Instant.parse("2026-01-01T00:00:00Z");
+ Assertions.assertThat(find("4.14.5", registriesDir,
clockAt(start)).version()).isEqualTo(CAMEL_4_14_5_PLATFORM);
+
+ // the registry now has a respin for the same Camel version; its BOM
is the same document as 3.27.3.1
+ wireMock.resetAll();
+
wireMock.stubFor(WireMock.get(WireMock.urlEqualTo("/client/platforms/all"))
+ .willReturn(WireMock.ok()
+ .withHeader("Content-Type", "application/json")
+ .withBody(Files.readString(
+
Path.of("target/test-classes/QuarkusHelperTest/quarkus-registry-client-platforms.json"))
+ .replace(CAMEL_4_14_5_PLATFORM, "3.27.4"))));
+ Function<MavenGav, MavenArtifact> resolver = gav ->
resolve(MavenGav.fromCoordinates(
+ gav.getGroupId(), gav.getArtifactId(),
+ "3.27.4".equals(gav.getVersion()) ? CAMEL_4_14_5_PLATFORM :
gav.getVersion(), "pom", null));
+ backdate(registryCacheFile(registriesDir));
+
+ QuarkusPlatformBom bom = find("4.14.5", resolver, true, false,
registriesDir,
+ clockAt(start.plus(Duration.ofDays(8))));
+
+ Assertions.assertThat(bom.version()).isEqualTo("3.27.4");
+ Assertions.assertThat(bom.camelVersion()).isEqualTo("4.14.5");
+ verifyRegistryRequests(1);
+
Assertions.assertThat(mappingFile(registriesDir)).content().contains("3.27.4").doesNotContain(CAMEL_4_14_5_PLATFORM);
+ }
+
+ @Test
+ void entryWithoutTimestampIsAMiss() throws Exception {
+ stubRegistry();
+ Path registriesDir = newRegistriesDir();
+ Path mappingFile = mappingFile(registriesDir);
+ Files.createDirectories(mappingFile.getParent());
+ Files.writeString(mappingFile,
+ "{\"4.14.5\":{\"groupId\":\"planted\",\"version\":\"0\"}}");
+
+ QuarkusPlatformBom bom = find("4.14.5", true, false, registriesDir);
+
+ Assertions.assertThat(bom.groupId()).isEqualTo("io.quarkus.platform");
+ Assertions.assertThat(bom.version()).isEqualTo(CAMEL_4_14_5_PLATFORM);
+ verifyRegistryRequests(1);
+
Assertions.assertThat(mappingFile).content().doesNotContain("planted").contains("resolvedAt");
+ }
+
+ @Test
+ void entryFromTheFutureIsAMiss() throws Exception {
+ stubRegistry();
+ Path registriesDir = newRegistriesDir();
+ Instant start = Instant.parse("2026-01-01T00:00:00Z");
+ find("4.14.5", registriesDir, clockAt(start.plus(Duration.ofDays(1))));
+ backdate(registryCacheFile(registriesDir));
+
+ // the clock went backwards since the entry was written
+ find("4.14.5", registriesDir, clockAt(start));
+
+ verifyRegistryRequests(2);
+ Assertions.assertThat(resolvedAt(mappingFile(registriesDir),
"4.14.5")).isEqualTo(start.toEpochMilli());
+ }
+
+ @Test
+ void storeLeavesNoTempFiles() throws Exception {
+ stubRegistry();
+ Path registriesDir = newRegistriesDir();
+
+ find("4.14.5", true, false, registriesDir);
+ find("4.14.5", true, true, registriesDir);
+
+ assertNoTempFiles(registriesDir);
+ try (Stream<Path> files =
Files.list(mappingFile(registriesDir).getParent())) {
+ Assertions.assertThat(files.map(f -> f.getFileName().toString()))
+ .containsExactlyInAnyOrder("all.json",
"platform-mapping.json");
+ }
+ }
}