diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/CreateProjectCommandHandler.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/CreateProjectCommandHandler.java index b541998637efa9..5c89d5ba766da2 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/CreateProjectCommandHandler.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/CreateProjectCommandHandler.java @@ -11,7 +11,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Properties; import java.util.Set; import java.util.stream.Collectors; @@ -71,10 +70,13 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws final List extensionOrigins = getExtensionOrigins(mainCatalog, extensionsToAdd); final List platformBoms = new ArrayList<>(Math.max(extensionOrigins.size(), 1)); - if (extensionOrigins.size() > 0) { + Map platformProjectData; + if (!extensionOrigins.isEmpty()) { // necessary to set the versions from the selected origins - extensionsToAdd = computeRequiredExtensions(CatalogMergeUtility.merge(extensionOrigins), extensionsQuery, - invocation.log()); + final ExtensionCatalog mergedCatalog = CatalogMergeUtility.merge(extensionOrigins); + platformProjectData = ToolsUtils.readProjectData(mergedCatalog); + setQuarkusProperties(invocation, mergedCatalog); + extensionsToAdd = computeRequiredExtensions(mergedCatalog, extensionsQuery, invocation.log()); // collect platform BOMs to import boolean sawFirstPlatform = false; for (ExtensionCatalog c : extensionOrigins) { @@ -88,7 +90,9 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws platformBoms.add(c.getBom()); } } else { + platformProjectData = ToolsUtils.readProjectData(mainCatalog); platformBoms.add(mainCatalog.getBom()); + setQuarkusProperties(invocation, mainCatalog); } final List extensionCoords = new ArrayList<>(extensionsToAdd.size()); @@ -107,13 +111,6 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws invocation.setValue(CatalogKey.BOM_ARTIFACT_ID, mainCatalog.getBom().getArtifactId()); invocation.setValue(CatalogKey.BOM_VERSION, mainCatalog.getBom().getVersion()); invocation.setValue(QUARKUS_VERSION, mainCatalog.getQuarkusCoreVersion()); - final Properties quarkusProps = ToolsUtils.readQuarkusProperties(mainCatalog); - quarkusProps.forEach((k, v) -> { - final String name = k.toString(); - if (!invocation.hasValue(name)) { - invocation.setValue(name, v.toString()); - } - }); try { Map platformData = new HashMap<>(); @@ -132,12 +129,17 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws .addCodestarts(invocation.getValue(EXTRA_CODESTARTS, Set.of())) .noBuildToolWrapper(invocation.getValue(NO_BUILDTOOL_WRAPPER, false)) .noDockerfiles(invocation.getValue(NO_DOCKERFILES, false)) + .addData(platformProjectData) .addData(platformData) .addData(toCodestartData(invocation.getValues())) .addData(invocation.getValue(DATA, Map.of())) .messageWriter(invocation.log()) .defaultCodestart(getDefaultCodestart(mainCatalog)) .build(); + + for (var e : input.getData().entrySet()) { + System.out.println("data: " + e.getKey() + "=" + e.getValue()); + } invocation.log().info("-----------"); if (!extensionsToAdd.isEmpty()) { invocation.log().info("selected extensions: \n" @@ -164,6 +166,17 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws return QuarkusCommandOutcome.success(); } + private static void setQuarkusProperties(QuarkusCommandInvocation invocation, ExtensionCatalog catalog) { + var quarkusProps = ToolsUtils.readQuarkusProperties(catalog); + quarkusProps.forEach((k, v) -> { + final String name = k.toString(); + System.out.println("VALUE " + name + " " + v); + if (!invocation.hasValue(name)) { + invocation.setValue(name, v.toString()); + } + }); + } + private List computeRequiredExtensions(ExtensionCatalog catalog, final Set extensionsQuery, MessageWriter log) throws QuarkusCommandException { final List extensionsToAdd = computeExtensionsFromQuery(catalog, extensionsQuery, log); diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/tools/ToolsUtils.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/tools/ToolsUtils.java index 7156237985f9f6..4ff64bee46ca3c 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/tools/ToolsUtils.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/platform/tools/ToolsUtils.java @@ -232,6 +232,12 @@ public static Properties readQuarkusProperties(ExtensionCatalog catalog) { return properties; } + @SuppressWarnings("unchecked") + public static Map readProjectData(ExtensionCatalog catalog) { + Map map = (Map) catalog.getMetadata().getOrDefault("project", Map.of()); + return (Map) map.getOrDefault("data", Map.of()); + } + public static String requireProperty(Properties props, String name) { final String value = props.getProperty(name); if (value == null) { diff --git a/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/registry/client/TestRegistryClientBuilder.java b/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/registry/client/TestRegistryClientBuilder.java index 365953ff669b68..8e0171379b0c82 100644 --- a/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/registry/client/TestRegistryClientBuilder.java +++ b/independent-projects/tools/devtools-testing/src/main/java/io/quarkus/devtools/testing/registry/client/TestRegistryClientBuilder.java @@ -229,7 +229,7 @@ public void build() { throw new IllegalStateException("Failed to serialize registry client configuration " + configYaml, e); } - externalCodestartBuilders.forEach(b -> b.persist()); + externalCodestartBuilders.forEach(TestCodestartBuilder::persist); installExtensionArtifacts(externalExtensions); } @@ -673,6 +673,7 @@ public static class TestPlatformCatalogMemberBuilder { private final TestPlatformCatalogReleaseBuilder release; private final ExtensionCatalog.Mutable extensions = ExtensionCatalog.builder(); + private Map codestartArtifacts; private final Model pom; private TestPlatformCatalogMemberBuilder(TestPlatformCatalogReleaseBuilder release, ArtifactCoords bom) { @@ -740,11 +741,24 @@ public TestPlatformCatalogMemberBuilder addExtension(String artifactId) { } public TestPlatformCatalogMemberBuilder addExtension(String groupId, String artifactId, String version) { + return addExtensionWithCodestart(groupId, artifactId, version, null); + } + + public TestPlatformCatalogMemberBuilder addExtensionWithCodestart(String artifactId, String codestart) { + return addExtensionWithCodestart(extensions.getBom().getGroupId(), artifactId, extensions.getBom().getVersion(), + codestart); + } + + public TestPlatformCatalogMemberBuilder addExtensionWithCodestart(String groupId, String artifactId, String version, + String codestart) { final ArtifactCoords coords = ArtifactCoords.jar(groupId, artifactId, version); final Extension.Mutable e = Extension.builder() .setArtifact(coords) .setName(artifactId) .setOrigins(Collections.singletonList(extensions)); + if (codestart != null) { + e.getMetadata().put("codestart", Map.of("name", codestart, "languages", List.of("java"))); + } extensions.addExtension(e); final Dependency d = new Dependency(); @@ -761,6 +775,22 @@ public TestPlatformCatalogMemberBuilder addExtension(String groupId, String arti return this; } + public TestPlatformCatalogMemberBuilder addCodestartsArtifact(ArtifactCoords coords, Path jarFile) { + if (codestartArtifacts == null) { + codestartArtifacts = new LinkedHashMap<>(); + } + codestartArtifacts.put(coords, jarFile); + return this; + } + + @SuppressWarnings("unchecked") + public TestPlatformCatalogMemberBuilder setPlatformProjectData(String name, Object value) { + var map = (Map) extensions.getMetadata().computeIfAbsent("project", k -> new HashMap<>()); + map = (Map) map.computeIfAbsent("data", k -> new HashMap<>()); + map.put(name, value); + return this; + } + public TestPlatformCatalogReleaseBuilder release() { return release; } @@ -781,6 +811,16 @@ private void install(ArtifactCoords coords, Path path) { } private void persist(Path memberDir) { + + if (codestartArtifacts != null) { + final List artifacts = new ArrayList<>(codestartArtifacts.size()); + for (var entry : codestartArtifacts.entrySet()) { + artifacts.add(entry.getKey().toGACTVString()); + install(entry.getKey(), entry.getValue()); + } + extensions.getMetadata().put("codestarts-artifacts", artifacts); + } + release.setReleaseInfo(extensions); final ArtifactCoords bom = extensions.getBom(); final Path json = getMemberCatalogPath(memberDir, bom); diff --git a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/project/create/PlatformWithoutQuarkusBomTest.java b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/project/create/PlatformWithoutQuarkusBomTest.java index c49ee7cac3fb8a..b79bfb405eaccf 100644 --- a/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/project/create/PlatformWithoutQuarkusBomTest.java +++ b/independent-projects/tools/devtools-testing/src/test/java/io/quarkus/devtools/project/create/PlatformWithoutQuarkusBomTest.java @@ -1,13 +1,25 @@ package io.quarkus.devtools.project.create; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import java.util.Map; +import java.util.Properties; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; import io.quarkus.devtools.testing.registry.client.TestRegistryClientBuilder; +import io.quarkus.fs.util.ZipUtils; import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.paths.PathTree; public class PlatformWithoutQuarkusBomTest extends MultiplePlatformBomsTestBase { @@ -15,6 +27,8 @@ public class PlatformWithoutQuarkusBomTest extends MultiplePlatformBomsTestBase @BeforeAll public static void setup() throws Exception { + + System.out.println(new BootstrapMavenContext().getWorkspace().getProjects().keySet()); TestRegistryClientBuilder.newInstance() //.debug() .baseDir(configDir()) @@ -26,6 +40,9 @@ public static void setup() throws Exception { .quarkusVersion("2.2.2") .newMember("acme-magic-bom") .addExtension("acme-magic") + .addExtensionWithCodestart("quarkus-property-dump", "property-dump") + .setPlatformProjectData("property-dump-codestart", + Map.of("acme", Map.of("source", Map.of("platform", "acme-platform")))) .release() .stream().platform() .registry() @@ -42,14 +59,55 @@ public static void setup() throws Exception { // default bom including quarkus-core + essential metadata .addCoreMember() .addExtension("quarkus-magic") + .addCodestartsArtifact(ArtifactCoords.jar("org.acme", "acme-codestarts", "1.0"), packageCodestart()) + .release() + .newMember("quarkus-zoo-bom") + .addExtension("quarkus-giraffe") .release() .registry() .clientBuilder() .build(); + System.setProperty("maven.repo.local.tail", TestRegistryClientBuilder.getMavenRepoDir(configDir()).toString()); enableRegistryClient(); } + private static Path packageCodestart() { + var url = Thread.currentThread().getContextClassLoader().getResource("codestarts"); + if (url == null) { + throw new RuntimeException("Failed to locate codestarts directory on the classpath"); + } + var dir = Path.of(url.getPath()); + if (!Files.isDirectory(dir)) { + throw new RuntimeException(dir + " is not a directory"); + } + + var codestartsJar = Path.of("target").resolve("test-codestarts.jar"); + try { + Files.deleteIfExists(codestartsJar); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + try (FileSystem fs = ZipUtils.newZip(codestartsJar)) { + var codestartsDir = fs.getPath("codestarts"); + Files.createDirectories(codestartsDir); + PathTree.ofDirectoryOrArchive(dir).walk(visit -> { + final String relativePath = visit.getRelativePath(); + if (relativePath.isEmpty()) { + return; + } + try { + Files.copy(visit.getPath(), codestartsDir.resolve(visit.getRelativePath())); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return codestartsJar; + } + protected String getMainPlatformKey() { return "org.quarkus.platform"; } @@ -86,4 +144,43 @@ public void testAcmePlatformMagic() throws Exception { List.of(ArtifactCoords.jar("org.acme.platform", "acme-magic", null)), "2.0.4"); } + + @Test + public void testMix() throws Exception { + final Path projectDir = newProjectDir("created-platform-wo-quarkus-bom"); + createProject(projectDir, List.of("giraffe", "acme-magic")); + + assertModel(projectDir, + List.of(mainPlatformBom(), + ArtifactCoords.pom("${quarkus.platform.group-id}", "quarkus-zoo-bom", "${quarkus.platform.version}"), + ArtifactCoords.pom(MAIN_PLATFORM_KEY, "acme-magic-bom", "7.0.7")), + List.of(ArtifactCoords.jar("org.acme.platform", "acme-magic", null), + ArtifactCoords.jar("org.quarkus.platform", "quarkus-giraffe", null)), + "2.0.4"); + } + + @Test + public void testAcmePropertyDump() throws Exception { + final Path projectDir = newProjectDir("created-platform-wo-quarkus-bom"); + createProject(projectDir, List.of("quarkus-property-dump")); + + assertModel(projectDir, + List.of(mainPlatformBom(), ArtifactCoords.pom(MAIN_PLATFORM_KEY, "acme-magic-bom", "7.0.7")), + List.of(ArtifactCoords.jar("org.acme.platform", "quarkus-property-dump", null)), + "2.0.4"); + + assertPropertyDump(projectDir, Map.of( + "acme.source.codestart", "codestart", + "acme.source.platform", "acme-platform")); + } + + private static void assertPropertyDump(Path projectDir, Map expectedProps) throws IOException { + Path propDump = projectDir.resolve("property-dump.txt"); + assertThat(propDump).exists(); + Properties props = new Properties(); + try (BufferedReader reader = Files.newBufferedReader(propDump)) { + props.load(reader); + } + assertThat(props).containsExactlyInAnyOrderEntriesOf(expectedProps); + } } diff --git a/independent-projects/tools/devtools-testing/src/test/resources/codestarts/quarkus/property-dump-codestart/base/property-dump.tpl.qute.txt b/independent-projects/tools/devtools-testing/src/test/resources/codestarts/quarkus/property-dump-codestart/base/property-dump.tpl.qute.txt new file mode 100644 index 00000000000000..daddd067510d0e --- /dev/null +++ b/independent-projects/tools/devtools-testing/src/test/resources/codestarts/quarkus/property-dump-codestart/base/property-dump.tpl.qute.txt @@ -0,0 +1,2 @@ +acme.source.codestart={acme.source.codestart} +acme.source.platform={acme.source.platform} diff --git a/independent-projects/tools/devtools-testing/src/test/resources/codestarts/quarkus/property-dump-codestart/codestart.yml b/independent-projects/tools/devtools-testing/src/test/resources/codestarts/quarkus/property-dump-codestart/codestart.yml new file mode 100644 index 00000000000000..59e7d94e9dc8c7 --- /dev/null +++ b/independent-projects/tools/devtools-testing/src/test/resources/codestarts/quarkus/property-dump-codestart/codestart.yml @@ -0,0 +1,14 @@ +name: property-dump-codestart +ref: property-dump +type: code +tags: extension-codestart +metadata: + title: Property Dump + description: Property dump. +language: + base: + data: + acme: + source: + codestart: codestart + platform: codestart \ No newline at end of file diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/CatalogMergeUtility.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/CatalogMergeUtility.java index e4f3bb3be8a666..92b90ad0649016 100644 --- a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/CatalogMergeUtility.java +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/CatalogMergeUtility.java @@ -6,6 +6,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import io.quarkus.maven.dependency.ArtifactKey; @@ -53,11 +54,12 @@ public static ExtensionCatalog merge(List catalogs) originCatalogs.put(catalog.getId(), catalog); }); + System.out.println("Merging roots"); for (ExtensionCatalog catalog : roots) { if (combined.getBom() == null) { combined.setBom(catalog.getBom()); } - + System.out.println("- " + catalog.getId()); if (catalog.getId() != null) { derivedFrom.putIfAbsent(catalog.getId(), catalog); } @@ -73,7 +75,9 @@ public static ExtensionCatalog merge(List catalogs) } }); - catalog.getMetadata().forEach(metadata::putIfAbsent); + for (var e : catalog.getMetadata().entrySet()) { + putIfAbscentRecursively(e.getKey(), e.getValue(), metadata); + } if (combined.getQuarkusCoreVersion() == null && catalog.getQuarkusCoreVersion() != null) { combined.setQuarkusCoreVersion(catalog.getQuarkusCoreVersion()); @@ -93,6 +97,41 @@ public static ExtensionCatalog merge(List catalogs) return combined.build(); } + /** + * Adds missing key-value pairs to the target map recursively, meaning + * if the current {@code value} is a map and the target map contains the corresponding {@code key} + * with a value that is also a map, {@link #putIfAbscentRecursively(Object, Object, Map)} + * will be called for each key-value pair of the current {@code value}. + * + * @param key key to put + * @param value value to put + * @param target target map + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static void putIfAbscentRecursively(Object key, Object value, Map target) { + target.compute(key, (k, currentValue) -> { + if (currentValue == null) { + return value; + } + if (Objects.equals(currentValue, value) + || !(value instanceof Map) + || !(currentValue instanceof Map currentMap)) { + return currentValue; + } + for (var e : ((Map) value).entrySet()) { + if (e.getKey() instanceof String) { + try { + putIfAbscentRecursively(e.getKey().toString(), e.getValue(), currentMap); + } catch (UnsupportedOperationException ex) { + currentMap = new HashMap(currentMap); + putIfAbscentRecursively(e.getKey().toString(), e.getValue(), currentMap); + } + } + } + return currentMap; + }); + } + // Package private. static PlatformCatalog mergePlatformCatalogs(List catalogs) { if (catalogs.isEmpty()) {