davsclaus commented on code in PR #23333: URL: https://github.com/apache/camel/pull/23333#discussion_r3275438769
########## dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/QuarkusPlatformMixin.java: ########## @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.dsl.jbang.core.commands; + +import java.util.Properties; +import java.util.function.Function; + +import org.apache.camel.dsl.jbang.core.common.QuarkusHelper; +import org.apache.camel.dsl.jbang.core.common.QuarkusHelper.QuarkusPlatformBom; +import org.apache.camel.tooling.maven.MavenArtifact; +import org.apache.camel.tooling.maven.MavenDownloader; +import org.apache.camel.tooling.maven.MavenGav; +import picocli.CommandLine; + +import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.QUARKUS_EXTENSION_REGISTRY_BASE_URI; +import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.QUARKUS_GROUP_ID; +import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.QUARKUS_VERSION; + +/** + * Options related to Quarkus Platform. + */ +public class QuarkusPlatformMixin extends QuarkusExtensionRegistryMixin implements QuarkusPlatformMixinSpec { + + @CommandLine.Option(names = { + "--quarkus-group-id" }, description = "groupId of Quarkus Platform BOM; honored only if --quarkus-version is set", + defaultValue = "io.quarkus.platform") + String quarkusGroupId = "io.quarkus.platform"; + + @CommandLine.Option(names = { + "--quarkus-artifact-id" }, + description = "Deprecated. This value is not used anymore. It is kept only for backwards compatibility and will be removed in Camel 5.x. Camel commands may use either 'quarkus-bom' or 'quarkus-camel-bom' artifactIds dependening on the context.", Review Comment: Minor typo: `dependening` → `depending`. ```suggestion description = "Deprecated. This value is not used anymore. It is kept only for backwards compatibility and will be removed in Camel 5.x. Camel commands may use either 'quarkus-bom' or 'quarkus-camel-bom' artifactIds depending on the context.", ``` ########## dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/update/CamelUpdateMixin.java: ########## @@ -49,13 +54,12 @@ public class CamelUpdateMixin { boolean debug; Review Comment: Same typo here: `Extensio Registry` → `Extension Registry` (missing 'n'). ```suggestion description = "The version of the Quarkus Maven plugin to use; the default value is looked up in Quarkus Extension Registry") ``` ########## dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyUpdateTest.java: ########## @@ -96,11 +97,16 @@ private void checkOneDependencyAddedForArangoDb(RuntimeType rt) throws Exception } } - private void checkNoUpdateOnFreshlyGeneratedproject() throws Exception { + private void checkNoUpdateOnFreshlyGeneratedproject(RuntimeType rt) throws Exception { DependencyUpdate command = new DependencyUpdate(new CamelJBangMain().withPrinter(printer)); CommandLine.populateCommand(command, "--dir=" + workingDir, + "--camel-version=4.13.0", + CamelCommandBaseTestSupport.quarkusExtRegistry(), new File(workingDir, "pom.xml").getAbsolutePath()); + if (rt == RuntimeType.main) { Review Comment: This looks like a debug leftover — `System.out.println("")` that prints an empty line conditionally. Consider removing it. ########## dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/QuarkusHelper.java: ########## @@ -16,150 +16,577 @@ */ package org.apache.camel.dsl.jbang.core.common; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.math.BigInteger; import java.net.URI; +import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Comparator; import java.util.List; -import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.Properties; import java.util.function.BiConsumer; -import java.util.function.BinaryOperator; import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.w3c.dom.Document; + +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import org.apache.camel.dsl.jbang.core.commands.version.VersionList.CamelAndRuntimeVersions; +import org.apache.camel.tooling.maven.MavenArtifact; +import org.apache.camel.tooling.maven.MavenDownloader; +import org.apache.camel.tooling.maven.MavenGav; +import org.apache.camel.util.json.DeserializationException; import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; import org.apache.camel.util.json.Jsoner; +import org.apache.maven.artifact.versioning.ComparableVersion; + +import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.QUARKUS_EXTENSION_REGISTRY_BASE_URI; +import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.QUARKUS_GROUP_ID; +import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.QUARKUS_VERSION; /** * Helper for resolving Quarkus platform information from the Quarkus registry. */ public final class QuarkusHelper { public static final String QUARKUS_PLATFORM_URL_PROPERTY = "camel.jbang.quarkus.platform.url"; - public static final String DEFAULT_QUARKUS_PLATFORM_URL = RuntimeType.QUARKUS_EXTENSION_REGISTRY_BASE_URL - + (RuntimeType.QUARKUS_EXTENSION_REGISTRY_BASE_URL.endsWith("/") - ? "" : "/") - + "client/platforms"; private QuarkusHelper() { } - /** - * Returns the Quarkus platform registry URL, honoring the system property {@value #QUARKUS_PLATFORM_URL_PROPERTY} - * if set. - */ - public static String getQuarkusPlatformUrl() { - return System.getProperty(QUARKUS_PLATFORM_URL_PROPERTY, DEFAULT_QUARKUS_PLATFORM_URL); + public static Stream<CamelAndRuntimeVersions> listQuarkusPlatformVersions( + Function<MavenGav, MavenArtifact> mavenResolver, + boolean download, + String quarkusExtensionRegistryBaseUri, + boolean fresh) { + JsonArray streams = fetchPlatformStreams(quarkusExtensionRegistryBaseUri, download, fresh, registriesDir()); + if (streams == null || streams.isEmpty()) { + return Stream.empty(); + } + return listQuarkusPlatformVersions(streams, mavenResolver); } - /** - * Resolves the actual Quarkus platform version for each row by fetching the Quarkus platform registry and matching - * the Camel Quarkus major.minor version against stream IDs. - * - * @param rows the list of rows to enrich with Quarkus platform versions - * @param runtimeVersionFunc function to extract the runtime (Camel Quarkus) version from a row - * @param quarkusVersionSetter consumer to set the resolved Quarkus platform version on a row - */ - public static <T> void resolveQuarkusPlatformVersions( - List<T> rows, - Function<T, String> runtimeVersionFunc, - BiConsumer<T, String> quarkusVersionSetter) { - - JsonArray streams = fetchPlatformStreams(); - if (streams == null) { - return; + private static Path registriesDir() { + final Path regDir = CommandLineHelper.getCamelDir().resolve("quarkus-extension-registries"); + try { + if (!Files.isDirectory(regDir)) { + Files.createDirectories(regDir); + } + return regDir + .toRealPath(LinkOption.NOFOLLOW_LINKS); + } catch (IOException e) { + throw new UncheckedIOException("Could not get real path for " + regDir, e); } + } - // keep the row with the highest runtime version per major.minor stream - BinaryOperator<T> keepLatest = (a, b) -> VersionHelper.compare( - runtimeVersionFunc.apply(a), runtimeVersionFunc.apply(b)) >= 0 ? a : b; - - Map<String, T> latestPerStream = rows.stream() - .filter(row -> runtimeVersionFunc.apply(row) != null) - .collect(Collectors.toMap( - row -> VersionHelper.getMajorMinorVersion(runtimeVersionFunc.apply(row)), - Function.identity(), - keepLatest)); - - // match each major.minor against registry streams and set the quarkus version - latestPerStream.forEach((majorMinor, row) -> findStreamVersion(streams, majorMinor, "quarkus-core-version") - .ifPresent(version -> quarkusVersionSetter.accept(row, version))); + static Stream<CamelAndRuntimeVersions> listQuarkusPlatformVersions( + JsonArray streams, + Function<MavenGav, MavenArtifact> mavenResolver) { + return streams.stream() + .map(s -> (JsonObject) s) + .map(PlatformStream::of) + .flatMap(platformStream -> platformStream.platformReleases().stream()) + .map(platformRelease -> { + List<String> versions + = platformRelease.findManagedVersions(mavenResolver, Ga.CAMEL_DIRECT, Ga.CAMEL_QUARKUS_DIRECT); + return new CamelAndRuntimeVersions( + versions.get(0), platformRelease.quarkusCamelBomGav.getVersion(), versions.get(1)); + }); } /** - * Resolves the Quarkus platform BOM version by matching the build-time version's major.minor against the Quarkus - * platform registry. + * Finds the newest Quarkus platform BOM {@code g:a:v} using the same or newer {@code major.minor} Camel version as + * the specified {@code camelVersion} by searching in Quarkus platform registry or returns the specified + * {@code buildTimeQuarkusVersion} if no compatible Platform version can be found. * <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. * - * @param buildTimeVersion the build-time Quarkus version (e.g., "3.15.7.something") - * @return the resolved platform BOM version from the registry, or the original buildTimeVersion if - * resolution fails + * @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 + * {@code null} + * @param quarkusExtensionRegistryBaseUri + * @param downloader A {@link MavenDownloader} to use for accessing + * @return the resolved platform BOM version from the registry, or the original + * buildTimeVersion if resolution fails */ - public static String resolveQuarkusPlatformVersion(String buildTimeVersion) { - if (buildTimeVersion == null) { - return null; + public static QuarkusPlatformBom findQuarkusPlatformBom( + String camelVersion, + Function<MavenGav, MavenArtifact> mavenResolver, + boolean download, + String quarkusExtensionRegistryBaseUri, + boolean fresh) { + if (camelVersion == null) { + camelVersion = RuntimeType.main.version(); } - JsonArray streams = fetchPlatformStreams(); - if (streams == null) { - return buildTimeVersion; + JsonArray streams = fetchPlatformStreams(quarkusExtensionRegistryBaseUri, download, fresh, registriesDir()); + if (streams == null || streams.isEmpty()) { + return null; } + Optional<QuarkusPlatformBom> resolved + = findPlatformBom(streams, new MajorMinor(camelVersion), mavenResolver, quarkusExtensionRegistryBaseUri); + return resolved.orElse(null); + } - String majorMinor = VersionHelper.getMajorMinorVersion(buildTimeVersion); - Optional<String> resolved = findStreamVersion(streams, majorMinor, "version"); - return resolved.orElse(buildTimeVersion); + public static String resolveCamelVersionFromQuarkusCamelBom( + MavenGav quarkusCamelBom, Function<MavenGav, MavenArtifact> mavenResolver) { + Path file = mavenResolver.apply(quarkusCamelBom).getFile().toPath(); + XPath xPath = XPathFactory.newInstance().newXPath(); + String expr = managedDependencyVersionXPath("org.apache.camel", "camel-direct"); + try (InputStream in = Files.newInputStream(file)) { + String camelVersion = (String) xPath.evaluate(expr, new InputSource(in), XPathConstants.STRING); + return camelVersion; + } catch (IOException e) { + throw new UncheckedIOException("Could not read " + file, e); + } catch (XPathExpressionException e) { + throw new RuntimeException("Could not evaluate " + expr + " on file " + file); + } } /** * Fetches the platform streams array from the Quarkus platform registry. * * @return the streams JsonArray, or null if the registry is unreachable or the response is invalid */ - private static JsonArray fetchPlatformStreams() { + static JsonArray fetchPlatformStreams( + String quarkusExtensionRegistryBaseUri, + boolean download, + boolean fresh, + Path registriesDir) { + final String quarkusPlatformUrl = quarkusExtensionRegistryBaseUri + + "/client/platforms/all" + + (quarkusExtensionRegistryBaseUri.startsWith("file://") ? ".json" : ""); try { + final URI uri = new URI(quarkusPlatformUrl); + if (uri.getScheme().equals("file")) { + return deserialize(Files.readString(Path.of(uri))); + } + + if (fresh && !download) { + throw new IllegalStateException( + "Options --fresh and --download=false contradict each other." + + " You may want to remove one of them or pass and explicit --quarkus-version if the current command supports it"); + } + + if (!fresh || !download) { + Path cacheFile = cacheFile(uri, registriesDir); + /* Check whether there is a cached document */ + if (Files.isRegularFile(cacheFile)) { + + if (!download || updatedToday(cacheFile)) { + /* We are in offline mode or the file was already updated today */ + return deserialize(Files.readString(cacheFile)); + } + } + } + HttpClient hc = HttpClient.newHttpClient(); HttpResponse<String> res = hc.send( - HttpRequest.newBuilder(new URI(getQuarkusPlatformUrl())) + HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(2)) .build(), HttpResponse.BodyHandlers.ofString()); if (res.statusCode() == 200) { - JsonObject json = (JsonObject) Jsoner.deserialize(res.body()); - JsonArray platforms = json.getCollection("platforms"); - if (platforms == null || platforms.isEmpty()) { - return null; - } - JsonObject platform = platforms.getMap(0); - return platform.getCollection("streams"); + final String jsonBody = res.body(); + + /* Refresh the cached file */ + Path cacheFile = cacheFile(uri, registriesDir); + Files.createDirectories(cacheFile.getParent()); + Files.writeString(cacheFile, jsonBody); + + return deserialize(jsonBody); + } else { + throw new RuntimeException( + "Could not get " + uri + ": " + res.statusCode() + " to determine the Quarkus Platform version." + + " You may want to pass --download=false to use the cached resource if available" + + " or pass and explicit --quarkus-version if the current command supports it"); } - } catch (Exception e) { - // ignore - if the registry is not reachable within 2 seconds, return null + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted when fetching from " + quarkusPlatformUrl, e); + } catch (IOException e) { + throw new RuntimeException("Could not fetch from " + quarkusPlatformUrl, e); + } catch (URISyntaxException e) { + throw new RuntimeException("Unvalid URI " + quarkusPlatformUrl, e); Review Comment: `Unvalid` is not a standard English word — should be `Invalid`. ```suggestion throw new RuntimeException("Invalid URI " + quarkusPlatformUrl, e); } catch (DeserializationException e) { throw new RuntimeException("Invalid JSON input from " + quarkusPlatformUrl, e); ``` ########## tooling/camel-tooling-maven/src/main/java/org/apache/camel/tooling/maven/MavenGav.java: ########## @@ -154,6 +154,25 @@ public static MavenGav parseGav(String gav, String defaultVersion) { return answer; } + /** + * @param gav {@code <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>} + * @return a new {@link MavenGav} + */ + public static MavenGav parseMaveResolverString(String gaecv) { Review Comment: Typo in public API method name: `parseMaveResolverString` — missing 'n' in "Maven". Should be `parseMavenResolverString`. Also, the `@param` Javadoc says `gav` but the actual parameter is named `gaecv` — these should match. ```suggestion public static MavenGav parseMavenResolverString(String gaecv) { ``` ########## dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportQuarkus.java: ########## @@ -372,27 +394,44 @@ private void createMavenPom(Path settings, Path pom, Set<String> deps) throws Ex Files.writeString(pom, context); } - private List<Map<String, Object>> buildQuarkusDependencyList(Set<String> deps, CamelCatalog catalog) { + private List<Map<String, Object>> buildQuarkusDependencyList(Set<String> deps, MavenGav quarkusCamelBom) { List<MavenGav> gavs = new ArrayList<>(); - for (String dep : deps) { - MavenGav gav = parseMavenGav(dep); - String gid = gav.getGroupId(); - String aid = gav.getArtifactId(); - // transform to camel-quarkus extension GAV - if ("org.apache.camel".equals(gid)) { - String qaid = aid.replace("camel-", "camel-quarkus-"); - ArtifactModel<?> am = catalog.modelFromMavenGAV("org.apache.camel.quarkus", qaid, null); - if (am != null) { - // use quarkus extension - gav.setGroupId(am.getGroupId()); - gav.setArtifactId(am.getArtifactId()); - gav.setVersion(null); // uses BOM so version should not be included - } else { - // there is no quarkus extension so use plain camel - gav.setVersion(camelVersion); + + Path file = mavenResolver.downloader().resolveArtifact(quarkusCamelBom).getFile().toPath(); + DocumentBuilderFactory dbf = XmlHelper.createDocumentBuilderFactory(); + + try (InputStream in = Files.newInputStream(file)) { + DocumentBuilder db = dbf.newDocumentBuilder(); + Document dom = db.parse(in); + XPath xPath = XPathFactory.newDefaultInstance().newXPath(); + for (String dep : deps) { + MavenGav gav = parseMavenGav(dep); + String gid = gav.getGroupId(); + String aid = gav.getArtifactId(); + // transform to camel-quarkus extension GAV + if ("org.apache.camel".equals(gid)) { + String qaid = aid.replace("camel-", "camel-quarkus-"); + if (nodeExists(file, dom, xPath, "org.apache.camel.quarkus", qaid)) { + // use quarkus extension + gav.setGroupId("org.apache.camel.quarkus"); + gav.setArtifactId(qaid); + gav.setVersion(null); // uses BOM so version should not be included + } else if (nodeExists(file, dom, xPath, "org.apache.camel", aid)) { + // The Camel artifact is managed in Quarkus Camel BOM + gav.setVersion(null); + } else { + // Use plain camel with hard-coded version + gav.setVersion(camelVersion); + } } + gavs.add(gav); } - gavs.add(gav); + } catch (IOException e) { + throw new UncheckedIOException("Could not read " + file, e); + } catch (SAXException e) { + throw new RuntimeException("Could parse " + file, e); Review Comment: Missing "not" in error messages — these currently read as if parsing succeeded: ```suggestion throw new RuntimeException("Could not parse " + file, e); } catch (ParserConfigurationException e) { throw new RuntimeException("Could not build a DocumentBuilder", e); ``` ########## dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/QuarkusPlatformMixin.java: ########## @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.dsl.jbang.core.commands; + +import java.util.Properties; +import java.util.function.Function; + +import org.apache.camel.dsl.jbang.core.common.QuarkusHelper; +import org.apache.camel.dsl.jbang.core.common.QuarkusHelper.QuarkusPlatformBom; +import org.apache.camel.tooling.maven.MavenArtifact; +import org.apache.camel.tooling.maven.MavenDownloader; +import org.apache.camel.tooling.maven.MavenGav; +import picocli.CommandLine; + +import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.QUARKUS_EXTENSION_REGISTRY_BASE_URI; +import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.QUARKUS_GROUP_ID; +import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.QUARKUS_VERSION; + +/** + * Options related to Quarkus Platform. + */ +public class QuarkusPlatformMixin extends QuarkusExtensionRegistryMixin implements QuarkusPlatformMixinSpec { + + @CommandLine.Option(names = { + "--quarkus-group-id" }, description = "groupId of Quarkus Platform BOM; honored only if --quarkus-version is set", + defaultValue = "io.quarkus.platform") + String quarkusGroupId = "io.quarkus.platform"; + + @CommandLine.Option(names = { + "--quarkus-artifact-id" }, + description = "Deprecated. This value is not used anymore. It is kept only for backwards compatibility and will be removed in Camel 5.x. Camel commands may use either 'quarkus-bom' or 'quarkus-camel-bom' artifactIds dependening on the context.", + defaultValue = "quarkus-bom") + @Deprecated(forRemoval = true, since = "4.21.0") + String quarkusArtifactId = "quarkus-bom"; + + @CommandLine.Option(names = { + "--quarkus-version" }, + description = "version of Quarkus Platform BOM; the default value is looked up in Quarkus Extensio Registry") Review Comment: Typo: `Extensio Registry` → `Extension Registry` (missing 'n'). ```suggestion description = "version of Quarkus Platform BOM; the default value is looked up in Quarkus Extension Registry") ``` ########## dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/QuarkusHelper.java: ########## @@ -16,150 +16,577 @@ */ package org.apache.camel.dsl.jbang.core.common; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.math.BigInteger; import java.net.URI; +import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Comparator; import java.util.List; -import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.Properties; import java.util.function.BiConsumer; -import java.util.function.BinaryOperator; import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.w3c.dom.Document; + +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import org.apache.camel.dsl.jbang.core.commands.version.VersionList.CamelAndRuntimeVersions; +import org.apache.camel.tooling.maven.MavenArtifact; +import org.apache.camel.tooling.maven.MavenDownloader; +import org.apache.camel.tooling.maven.MavenGav; +import org.apache.camel.util.json.DeserializationException; import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; import org.apache.camel.util.json.Jsoner; +import org.apache.maven.artifact.versioning.ComparableVersion; + +import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.QUARKUS_EXTENSION_REGISTRY_BASE_URI; +import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.QUARKUS_GROUP_ID; +import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.QUARKUS_VERSION; /** * Helper for resolving Quarkus platform information from the Quarkus registry. */ public final class QuarkusHelper { public static final String QUARKUS_PLATFORM_URL_PROPERTY = "camel.jbang.quarkus.platform.url"; - public static final String DEFAULT_QUARKUS_PLATFORM_URL = RuntimeType.QUARKUS_EXTENSION_REGISTRY_BASE_URL - + (RuntimeType.QUARKUS_EXTENSION_REGISTRY_BASE_URL.endsWith("/") - ? "" : "/") - + "client/platforms"; private QuarkusHelper() { } - /** - * Returns the Quarkus platform registry URL, honoring the system property {@value #QUARKUS_PLATFORM_URL_PROPERTY} - * if set. - */ - public static String getQuarkusPlatformUrl() { - return System.getProperty(QUARKUS_PLATFORM_URL_PROPERTY, DEFAULT_QUARKUS_PLATFORM_URL); + public static Stream<CamelAndRuntimeVersions> listQuarkusPlatformVersions( + Function<MavenGav, MavenArtifact> mavenResolver, + boolean download, + String quarkusExtensionRegistryBaseUri, + boolean fresh) { + JsonArray streams = fetchPlatformStreams(quarkusExtensionRegistryBaseUri, download, fresh, registriesDir()); + if (streams == null || streams.isEmpty()) { + return Stream.empty(); + } + return listQuarkusPlatformVersions(streams, mavenResolver); } - /** - * Resolves the actual Quarkus platform version for each row by fetching the Quarkus platform registry and matching - * the Camel Quarkus major.minor version against stream IDs. - * - * @param rows the list of rows to enrich with Quarkus platform versions - * @param runtimeVersionFunc function to extract the runtime (Camel Quarkus) version from a row - * @param quarkusVersionSetter consumer to set the resolved Quarkus platform version on a row - */ - public static <T> void resolveQuarkusPlatformVersions( - List<T> rows, - Function<T, String> runtimeVersionFunc, - BiConsumer<T, String> quarkusVersionSetter) { - - JsonArray streams = fetchPlatformStreams(); - if (streams == null) { - return; + private static Path registriesDir() { + final Path regDir = CommandLineHelper.getCamelDir().resolve("quarkus-extension-registries"); + try { + if (!Files.isDirectory(regDir)) { + Files.createDirectories(regDir); + } + return regDir + .toRealPath(LinkOption.NOFOLLOW_LINKS); + } catch (IOException e) { + throw new UncheckedIOException("Could not get real path for " + regDir, e); } + } - // keep the row with the highest runtime version per major.minor stream - BinaryOperator<T> keepLatest = (a, b) -> VersionHelper.compare( - runtimeVersionFunc.apply(a), runtimeVersionFunc.apply(b)) >= 0 ? a : b; - - Map<String, T> latestPerStream = rows.stream() - .filter(row -> runtimeVersionFunc.apply(row) != null) - .collect(Collectors.toMap( - row -> VersionHelper.getMajorMinorVersion(runtimeVersionFunc.apply(row)), - Function.identity(), - keepLatest)); - - // match each major.minor against registry streams and set the quarkus version - latestPerStream.forEach((majorMinor, row) -> findStreamVersion(streams, majorMinor, "quarkus-core-version") - .ifPresent(version -> quarkusVersionSetter.accept(row, version))); + static Stream<CamelAndRuntimeVersions> listQuarkusPlatformVersions( + JsonArray streams, + Function<MavenGav, MavenArtifact> mavenResolver) { + return streams.stream() + .map(s -> (JsonObject) s) + .map(PlatformStream::of) + .flatMap(platformStream -> platformStream.platformReleases().stream()) + .map(platformRelease -> { + List<String> versions + = platformRelease.findManagedVersions(mavenResolver, Ga.CAMEL_DIRECT, Ga.CAMEL_QUARKUS_DIRECT); + return new CamelAndRuntimeVersions( + versions.get(0), platformRelease.quarkusCamelBomGav.getVersion(), versions.get(1)); + }); } /** - * Resolves the Quarkus platform BOM version by matching the build-time version's major.minor against the Quarkus - * platform registry. + * Finds the newest Quarkus platform BOM {@code g:a:v} using the same or newer {@code major.minor} Camel version as + * the specified {@code camelVersion} by searching in Quarkus platform registry or returns the specified + * {@code buildTimeQuarkusVersion} if no compatible Platform version can be found. * <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. * - * @param buildTimeVersion the build-time Quarkus version (e.g., "3.15.7.something") - * @return the resolved platform BOM version from the registry, or the original buildTimeVersion if - * resolution fails + * @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 + * {@code null} + * @param quarkusExtensionRegistryBaseUri + * @param downloader A {@link MavenDownloader} to use for accessing + * @return the resolved platform BOM version from the registry, or the original + * buildTimeVersion if resolution fails */ - public static String resolveQuarkusPlatformVersion(String buildTimeVersion) { - if (buildTimeVersion == null) { - return null; + public static QuarkusPlatformBom findQuarkusPlatformBom( + String camelVersion, + Function<MavenGav, MavenArtifact> mavenResolver, + boolean download, + String quarkusExtensionRegistryBaseUri, + boolean fresh) { + if (camelVersion == null) { + camelVersion = RuntimeType.main.version(); } - JsonArray streams = fetchPlatformStreams(); - if (streams == null) { - return buildTimeVersion; + JsonArray streams = fetchPlatformStreams(quarkusExtensionRegistryBaseUri, download, fresh, registriesDir()); + if (streams == null || streams.isEmpty()) { + return null; } + Optional<QuarkusPlatformBom> resolved + = findPlatformBom(streams, new MajorMinor(camelVersion), mavenResolver, quarkusExtensionRegistryBaseUri); + return resolved.orElse(null); + } - String majorMinor = VersionHelper.getMajorMinorVersion(buildTimeVersion); - Optional<String> resolved = findStreamVersion(streams, majorMinor, "version"); - return resolved.orElse(buildTimeVersion); + public static String resolveCamelVersionFromQuarkusCamelBom( + MavenGav quarkusCamelBom, Function<MavenGav, MavenArtifact> mavenResolver) { + Path file = mavenResolver.apply(quarkusCamelBom).getFile().toPath(); + XPath xPath = XPathFactory.newInstance().newXPath(); + String expr = managedDependencyVersionXPath("org.apache.camel", "camel-direct"); + try (InputStream in = Files.newInputStream(file)) { + String camelVersion = (String) xPath.evaluate(expr, new InputSource(in), XPathConstants.STRING); + return camelVersion; + } catch (IOException e) { + throw new UncheckedIOException("Could not read " + file, e); + } catch (XPathExpressionException e) { + throw new RuntimeException("Could not evaluate " + expr + " on file " + file); + } } /** * Fetches the platform streams array from the Quarkus platform registry. * * @return the streams JsonArray, or null if the registry is unreachable or the response is invalid */ - private static JsonArray fetchPlatformStreams() { + static JsonArray fetchPlatformStreams( + String quarkusExtensionRegistryBaseUri, + boolean download, + boolean fresh, + Path registriesDir) { + final String quarkusPlatformUrl = quarkusExtensionRegistryBaseUri + + "/client/platforms/all" + + (quarkusExtensionRegistryBaseUri.startsWith("file://") ? ".json" : ""); try { + final URI uri = new URI(quarkusPlatformUrl); + if (uri.getScheme().equals("file")) { + return deserialize(Files.readString(Path.of(uri))); + } + + if (fresh && !download) { + throw new IllegalStateException( + "Options --fresh and --download=false contradict each other." + + " You may want to remove one of them or pass and explicit --quarkus-version if the current command supports it"); + } + + if (!fresh || !download) { + Path cacheFile = cacheFile(uri, registriesDir); + /* Check whether there is a cached document */ + if (Files.isRegularFile(cacheFile)) { + + if (!download || updatedToday(cacheFile)) { + /* We are in offline mode or the file was already updated today */ + return deserialize(Files.readString(cacheFile)); + } + } + } + HttpClient hc = HttpClient.newHttpClient(); HttpResponse<String> res = hc.send( - HttpRequest.newBuilder(new URI(getQuarkusPlatformUrl())) + HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(2)) .build(), HttpResponse.BodyHandlers.ofString()); if (res.statusCode() == 200) { - JsonObject json = (JsonObject) Jsoner.deserialize(res.body()); - JsonArray platforms = json.getCollection("platforms"); - if (platforms == null || platforms.isEmpty()) { - return null; - } - JsonObject platform = platforms.getMap(0); - return platform.getCollection("streams"); + final String jsonBody = res.body(); + + /* Refresh the cached file */ + Path cacheFile = cacheFile(uri, registriesDir); + Files.createDirectories(cacheFile.getParent()); + Files.writeString(cacheFile, jsonBody); + + return deserialize(jsonBody); + } else { + throw new RuntimeException( + "Could not get " + uri + ": " + res.statusCode() + " to determine the Quarkus Platform version." + + " You may want to pass --download=false to use the cached resource if available" + + " or pass and explicit --quarkus-version if the current command supports it"); } - } catch (Exception e) { - // ignore - if the registry is not reachable within 2 seconds, return null + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted when fetching from " + quarkusPlatformUrl, e); + } catch (IOException e) { + throw new RuntimeException("Could not fetch from " + quarkusPlatformUrl, e); + } catch (URISyntaxException e) { + throw new RuntimeException("Unvalid URI " + quarkusPlatformUrl, e); + } catch (DeserializationException e) { + throw new RuntimeException("Unvalid JSON input from " + quarkusPlatformUrl, e); + } + } + + private static boolean updatedToday(Path cacheFile) { + try { + FileTime lastModified = Files.getLastModifiedTime(cacheFile); + final ZoneId zone = ZoneId.systemDefault(); + final Instant startOfToday = LocalDate.now(zone) + .atStartOfDay(zone) + .toInstant(); + + return !lastModified.toInstant().isBefore(startOfToday); + } catch (IOException e) { + throw new UncheckedIOException("Could get last modified time of " + cacheFile, e); Review Comment: Missing "not" — should read "Could **not** get last modified time". ```suggestion throw new UncheckedIOException("Could not get last modified time of " + cacheFile, e); ``` -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
