diff --git a/devtools/cli/src/main/java/io/quarkus/cli/Create.java b/devtools/cli/src/main/java/io/quarkus/cli/Create.java index a1decff74abdc5..df01891e85ba31 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/Create.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/Create.java @@ -49,7 +49,7 @@ static class TargetBuildTool { boolean gradle = false; @CommandLine.Option(names = { - "--grade-kotlin-dsl" }, order = 7, description = "Create a Gradle Kotlin DSL project.") + "--gradle-kotlin-dsl" }, order = 7, description = "Create a Gradle Kotlin DSL project.") boolean gradleKotlinDsl = false; } diff --git a/devtools/cli/src/main/java/io/quarkus/cli/QuarkusCliUtils.java b/devtools/cli/src/main/java/io/quarkus/cli/QuarkusCliUtils.java index 8649029f2909e0..13fa57d06f33a2 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/QuarkusCliUtils.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/QuarkusCliUtils.java @@ -30,7 +30,6 @@ static QuarkusProject getQuarkusProject(BuildTool buildTool, Path projectRoot) { } private static QuarkusProject getNonMavenProject(Path projectRoot, BuildTool buildTool) { - return QuarkusProjectHelper.getProject(projectRoot, buildTool, - QuarkusCliVersion.version()); + return QuarkusProjectHelper.getProject(projectRoot, buildTool, null); } } diff --git a/devtools/gradle/src/main/java/io/quarkus/devtools/project/buildfile/GradleGroovyProjectBuildFile.java b/devtools/gradle/src/main/java/io/quarkus/devtools/project/buildfile/GradleGroovyProjectBuildFile.java index 2aa05f33cac59d..b5e7673c0296d8 100644 --- a/devtools/gradle/src/main/java/io/quarkus/devtools/project/buildfile/GradleGroovyProjectBuildFile.java +++ b/devtools/gradle/src/main/java/io/quarkus/devtools/project/buildfile/GradleGroovyProjectBuildFile.java @@ -25,6 +25,11 @@ String getBuildGradlePath() { return BUILD_GRADLE_PATH; } + @Override + protected boolean importBom(ArtifactCoords coords) { + return importBomInModel(getModel(), coords); + } + @Override protected boolean addDependency(ArtifactCoords coords, boolean managed) { return addDependencyInModel(getModel(), coords, managed); @@ -35,6 +40,12 @@ public BuildTool getBuildTool() { return BuildTool.GRADLE; } + static boolean importBomInModel(Model model, ArtifactCoords coords) { + return addDependencyInModel(model, + String.format(" implementation enforcedPlatform(%s)%n", + createDependencyCoordinatesString(coords, false, '\''))); + } + static boolean addDependencyInModel(Model model, ArtifactCoords coords, boolean managed) { return addDependencyInModel(model, String.format(" implementation %s%n", createDependencyCoordinatesString(coords, managed, '\''))); diff --git a/devtools/gradle/src/main/java/io/quarkus/devtools/project/buildfile/GradleKotlinProjectBuildFile.java b/devtools/gradle/src/main/java/io/quarkus/devtools/project/buildfile/GradleKotlinProjectBuildFile.java index f959d2b17dc27c..8dc4ff28f4a5f5 100644 --- a/devtools/gradle/src/main/java/io/quarkus/devtools/project/buildfile/GradleKotlinProjectBuildFile.java +++ b/devtools/gradle/src/main/java/io/quarkus/devtools/project/buildfile/GradleKotlinProjectBuildFile.java @@ -25,6 +25,11 @@ String getBuildGradlePath() { return BUILD_GRADLE_PATH; } + @Override + protected boolean importBom(ArtifactCoords coords) { + return importBomInModel(getModel(), coords); + } + @Override protected boolean addDependency(ArtifactCoords coords, boolean managed) { return addDependencyInModel(getModel(), coords, managed); @@ -35,6 +40,12 @@ public BuildTool getBuildTool() { return BuildTool.GRADLE_KOTLIN_DSL; } + static boolean importBomInModel(Model model, ArtifactCoords coords) { + return addDependencyInModel(model, + String.format(" implementation enforcedPlatform(%s)%n", + createDependencyCoordinatesString(coords, false, '\''))); + } + static boolean addDependencyInModel(Model model, ArtifactCoords coords, boolean managed) { return addDependencyInModel(model, String.format(" implementation(%s)%n", createDependencyCoordinatesString(coords, managed, '"'))); diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/builder/QuarkusModelBuilder.java b/devtools/gradle/src/main/java/io/quarkus/gradle/builder/QuarkusModelBuilder.java index 203f410d53518e..fc626465f9153a 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/builder/QuarkusModelBuilder.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/builder/QuarkusModelBuilder.java @@ -46,7 +46,9 @@ import org.gradle.tooling.provider.model.ParameterizedToolingModelBuilder; import io.quarkus.bootstrap.BootstrapConstants; -import io.quarkus.bootstrap.model.AppArtifactKey; +import io.quarkus.bootstrap.model.AppArtifactCoords; +import io.quarkus.bootstrap.model.PlatformReleases; +import io.quarkus.bootstrap.resolver.AppModelResolverException; import io.quarkus.bootstrap.resolver.model.ArtifactCoords; import io.quarkus.bootstrap.resolver.model.Dependency; import io.quarkus.bootstrap.resolver.model.ModelParameter; @@ -132,14 +134,18 @@ private Map resolvePlatformProperties(Project project, final Configuration boms = project.getConfigurations() .detachedConfiguration(deploymentDeps.toArray(new org.gradle.api.artifacts.Dependency[0])); final Map platformProps = new HashMap<>(); - final Set descriptorKeys = new HashSet<>(4); - final Set propertyKeys = new HashSet<>(2); + final Set descriptorCoords = new HashSet<>(4); + final Set propertyCoords = new HashSet<>(2); + // platform BOMs by platform keys (groupId) + final Map> importedPlatforms = new HashMap<>(); + final PlatformReleases platformReleases = new PlatformReleases(); boms.getResolutionStrategy().eachDependency(d -> { final String group = d.getTarget().getGroup(); final String name = d.getTarget().getName(); if (name.endsWith(BootstrapConstants.PLATFORM_DESCRIPTOR_ARTIFACT_ID_SUFFIX)) { - descriptorKeys.add(new AppArtifactKey(group, + descriptorCoords.add(new AppArtifactCoords(group, name.substring(0, name.length() - BootstrapConstants.PLATFORM_DESCRIPTOR_ARTIFACT_ID_SUFFIX.length()), + null, "pom", d.getTarget().getVersion())); } else if (name.endsWith(BootstrapConstants.PLATFORM_PROPERTIES_ARTIFACT_ID_SUFFIX)) { final DefaultDependencyArtifact dep = new DefaultDependencyArtifact(); @@ -151,6 +157,9 @@ private Map resolvePlatformProperties(Project project, group, name, d.getTarget().getVersion(), null); gradleDep.addArtifact(dep); + final String bomArtifactId = name.substring(0, + name.length() - BootstrapConstants.PLATFORM_PROPERTIES_ARTIFACT_ID_SUFFIX.length()); + for (ResolvedArtifact a : project.getConfigurations().detachedConfiguration(gradleDep) .getResolvedConfiguration().getResolvedArtifacts()) { if (a.getName().equals(name)) { @@ -163,26 +172,31 @@ private Map resolvePlatformProperties(Project project, for (Map.Entry prop : props.entrySet()) { final String propName = String.valueOf(prop.getKey()); if (propName.startsWith(BootstrapConstants.PLATFORM_PROPERTY_PREFIX)) { - platformProps.put(propName, String.valueOf(prop.getValue())); + if (PlatformReleases.isPlatformReleaseInfo(propName)) { + platformReleases.addPlatformRelease(propName, String.valueOf(prop.getValue())); + } else { + platformProps.put(propName, String.valueOf(prop.getValue())); + } } } break; } } - propertyKeys.add(new AppArtifactKey(group, - name.substring(0, name.length() - BootstrapConstants.PLATFORM_PROPERTIES_ARTIFACT_ID_SUFFIX.length()), - d.getTarget().getVersion())); + final AppArtifactCoords bomCoords = new AppArtifactCoords(group, bomArtifactId, null, "pom", + d.getTarget().getVersion()); + importedPlatforms.computeIfAbsent(group, g -> new ArrayList<>()).add(bomCoords); + propertyCoords.add(bomCoords); } }); boms.getResolvedConfiguration(); - if (!descriptorKeys.containsAll(propertyKeys)) { + if (!descriptorCoords.containsAll(propertyCoords)) { final StringBuilder buf = new StringBuilder(); buf.append( "The Quarkus platform properties applied to the project are missing the corresponding Quarkus platform BOM imports:"); final int l = buf.length(); - for (AppArtifactKey key : propertyKeys) { - if (!descriptorKeys.contains(key)) { + for (AppArtifactCoords key : propertyCoords) { + if (!descriptorCoords.contains(key)) { if (l - buf.length() < 0) { buf.append(','); } @@ -191,6 +205,11 @@ private Map resolvePlatformProperties(Project project, } throw new GradleException(buf.toString()); } + try { + platformReleases.assertAligned(importedPlatforms); + } catch (AppModelResolverException e) { + throw new GradleException("Failed to create the Quarkus Application Model: " + e.getMessage(), e); + } return platformProps; } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformInfo.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformInfo.java new file mode 100644 index 00000000000000..54121c54993467 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformInfo.java @@ -0,0 +1,72 @@ +package io.quarkus.bootstrap.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +public class PlatformInfo { + + private final String key; + private final List streams = new ArrayList<>(1); // most of the time there will be only one + + public PlatformInfo(String key) { + this.key = key; + } + + public String getPlatformKey() { + return key; + } + + public boolean isAligned(Collection importedBoms) { + if (streams.isEmpty()) { + return true; + } + if (streams.size() > 1) { + return false; + } + return streams.get(0).isAligned(importedBoms); + } + + public List> getPossibleAlignments(Collection importedPlatformBoms) { + if (streams.size() > 1) { + final StringBuilder buf = new StringBuilder(); + buf.append("Imported BOMs "); + final Iterator it = importedPlatformBoms.iterator(); + if (it.hasNext()) { + buf.append(it.next()); + while (it.hasNext()) { + buf.append(", ").append(it.next()); + } + } + buf.append(" belong to different platform streams ").append(streams.get(0)); + for (int i = 1; i < streams.size(); ++i) { + buf.append(", ").append(streams.get(i)); + } + throw new RuntimeException(buf.append(" while only one stream per platform is allowed.").toString()); + } + return streams.get(0).getPossibleAlignemnts(importedPlatformBoms); + } + + PlatformStreamInfo getOrCreateStream(String stream) { + PlatformStreamInfo s = getStream(stream); + if (s == null) { + s = new PlatformStreamInfo(stream); + streams.add(s); + } + return s; + } + + Collection getStreams() { + return streams; + } + + PlatformStreamInfo getStream(String stream) { + for (PlatformStreamInfo s : streams) { + if (s.getId().equals(stream)) { + return s; + } + } + return null; + } +} \ No newline at end of file diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformReleaseInfo.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformReleaseInfo.java new file mode 100644 index 00000000000000..768552dff40083 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformReleaseInfo.java @@ -0,0 +1,87 @@ +package io.quarkus.bootstrap.model; + +import java.util.ArrayList; +import java.util.List; + +/** + * Platform release info that is encoded into a property in a platform properties artifact + * following the format {@code platform.release-info@$#=(,)} + */ +public class PlatformReleaseInfo { + + private final String platformKey; + private final String stream; + private final String version; + private final List boms; + + PlatformReleaseInfo(String platformKey, String stream, String version, String boms) { + this.platformKey = platformKey; + this.stream = stream; + this.version = version; + final String[] bomCoords = boms.split(","); + this.boms = new ArrayList<>(bomCoords.length); + for (String s : bomCoords) { + this.boms.add(AppArtifactCoords.fromString(s)); + } + } + + /** + * The platform key. Could be the {@code groupId} of the stack, e.g. {@code io.quarkus.platform} + * + * @return platform key + */ + public String getPlatformKey() { + return platformKey; + } + + /** + * Platform stream. Could be the {@code major.minor} part of the platform release version. + * + * @return platform stream + */ + public String getStream() { + return stream; + } + + /** + * The version of the platform in a stream. Ideally, the micro version to make the comparisons easier. + * + * @return version in the stream + */ + public String getVersion() { + return version; + } + + /** + * Member BOM coordinates. + * + * @return member BOM coordinates + */ + public List getBoms() { + return boms; + } + + String getPropertyName() { + final StringBuilder buf = new StringBuilder(); + buf.append(PlatformReleases.PROPERTY_PREFIX).append(platformKey).append(PlatformReleases.PLATFORM_KEY_STREAM_SEPARATOR) + .append(stream) + .append(PlatformReleases.STREAM_VERSION_SEPARATOR).append(version); + return buf.toString(); + } + + String getPropertyValue() { + final StringBuilder buf = new StringBuilder(); + final List boms = getBoms(); + if (!boms.isEmpty()) { + buf.append(boms.get(0).toString()); + for (int i = 1; i < boms.size(); ++i) { + buf.append(',').append(boms.get(i)); + } + } + return buf.toString(); + } + + public String toString() { + return getPropertyName() + '=' + getPropertyValue(); + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformReleases.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformReleases.java new file mode 100644 index 00000000000000..37f68713e6d0ce --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformReleases.java @@ -0,0 +1,107 @@ +package io.quarkus.bootstrap.model; + +import io.quarkus.bootstrap.resolver.AppModelResolverException; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.StringWriter; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PlatformReleases { + + static final String PROPERTY_PREFIX = "platform.release-info@"; + + static final char PLATFORM_KEY_STREAM_SEPARATOR = '$'; + static final char STREAM_VERSION_SEPARATOR = '#'; + + private static int requiredIndex(String s, char c, int fromIndex) { + final int i = s.indexOf(c, fromIndex); + if (i < 0) { + throw new IllegalArgumentException("Failed to locate '" + c + "' in '" + s + "'"); + } + return i; + } + + public static boolean isPlatformReleaseInfo(String s) { + return s != null && s.startsWith(PROPERTY_PREFIX); + } + + private final Map platforms = new HashMap<>(); + + public PlatformReleases() { + } + + public void addPlatformRelease(String propertyName, String propertyValue) { + final int platformKeyStreamSep = requiredIndex(propertyName, PLATFORM_KEY_STREAM_SEPARATOR, PROPERTY_PREFIX.length()); + final int streamVersionSep = requiredIndex(propertyName, STREAM_VERSION_SEPARATOR, platformKeyStreamSep + 1); + + final String platformKey = propertyName.substring(PROPERTY_PREFIX.length(), platformKeyStreamSep); + final String streamId = propertyName.substring(platformKeyStreamSep + 1, streamVersionSep); + final String version = propertyName.substring(streamVersionSep + 1); + platforms.computeIfAbsent(platformKey, k -> new PlatformInfo(k)).getOrCreateStream(streamId).addIfNotPresent(version, + () -> new PlatformReleaseInfo(platformKey, streamId, version, propertyValue)); + } + + public void assertAligned(Map> importedPlatformBoms) + throws AppModelResolverException { + if (isAligned(importedPlatformBoms)) { + return; + } + final StringWriter buf = new StringWriter(); + try (BufferedWriter writer = new BufferedWriter(buf)) { + writer.append( + "Some of the imported Quarkus platform BOMs belong to different platform releases. To properly align the platform BOM imports, please, consider one of the following combinations:"); + writer.newLine(); + final Map>> possibleAlignments = getPossibleAlignemnts(importedPlatformBoms); + for (Map.Entry>> entry : possibleAlignments.entrySet()) { + writer.append("For platform ").append(entry.getKey()).append(':'); + writer.newLine(); + int i = 1; + for (List boms : entry.getValue()) { + writer.append(" ").append(String.valueOf(i++)).append(") "); + writer.newLine(); + for (String bom : boms) { + writer.append(" - ").append(bom); + writer.newLine(); + } + } + } + } catch (IOException e) { + // ignore + } + throw new AppModelResolverException(buf.toString()); + } + + public boolean isAligned(Map> importedPlatformBoms) { + for (Map.Entry> platformImportedBoms : importedPlatformBoms.entrySet()) { + final PlatformInfo platformInfo = platforms.get(platformImportedBoms.getKey()); + if (platformInfo != null && !platformInfo.isAligned(platformImportedBoms.getValue())) { + return false; + } + } + return true; + } + + public Map>> getPossibleAlignemnts( + Map> importedPlatformBoms) { + final Map>> alignments = new HashMap<>(importedPlatformBoms.size()); + for (Map.Entry> platformImportedBoms : importedPlatformBoms.entrySet()) { + final PlatformInfo platformInfo = platforms.get(platformImportedBoms.getKey()); + if (platformInfo == null || platformInfo.isAligned(platformImportedBoms.getValue())) { + continue; + } + alignments.put(platformInfo.getPlatformKey(), platformInfo.getPossibleAlignments(platformImportedBoms.getValue())); + } + return alignments; + } + + Collection getPlatforms() { + return platforms.values(); + } + + PlatformInfo getPlatform(String platformKey) { + return platforms.get(platformKey); + } +} diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformStreamInfo.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformStreamInfo.java new file mode 100644 index 00000000000000..afdc598c10696d --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformStreamInfo.java @@ -0,0 +1,77 @@ +package io.quarkus.bootstrap.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +public class PlatformStreamInfo { + + private final String id; + private final Map releases = new HashMap<>(); + + public PlatformStreamInfo(String stream) { + this.id = stream; + } + + public String getId() { + return id; + } + + public boolean isAligned(Collection importedBoms) { + if (releases.isEmpty()) { + return true; + } + for (PlatformReleaseInfo release : releases.values()) { + if (release.getBoms().containsAll(importedBoms)) { + return true; + } + } + return false; + } + + public List> getPossibleAlignemnts(Collection importedPlatformBoms) { + final Map importedKeys = new HashMap<>(importedPlatformBoms.size()); + for (AppArtifactCoords bom : importedPlatformBoms) { + importedKeys.put(bom.getKey(), bom.getVersion()); + } + final List> suggestions = new ArrayList<>(); + for (PlatformReleaseInfo release : releases.values()) { + final Map stackBoms = new HashMap<>(release.getBoms().size()); + for (AppArtifactCoords bom : release.getBoms()) { + stackBoms.put(bom.getKey(), bom); + } + if (stackBoms.keySet().containsAll(importedKeys.keySet())) { + final List suggestion = new ArrayList<>(importedPlatformBoms.size()); + suggestions.add(suggestion); + for (Map.Entry bomKey : importedKeys.entrySet()) { + final AppArtifactCoords stackBom = stackBoms.get(bomKey.getKey()); + if (!bomKey.getValue().equals(stackBom.getVersion())) { + suggestion.add(bomKey.getKey().getGroupId() + ":" + bomKey.getKey().getArtifactId() + ":" + + bomKey.getValue() + " -> " + stackBom.getVersion()); + } else { + suggestion + .add(stackBom.getGroupId() + ":" + stackBom.getArtifactId() + ":" + stackBom.getVersion()); + } + } + } + } + return suggestions; + } + + void addIfNotPresent(String version, Supplier release) { + if (!releases.containsKey(version)) { + releases.put(version, release.get()); + } + } + + Collection getReleases() { + return releases.values(); + } + + PlatformReleaseInfo getRelease(String version) { + return releases.get(version); + } +} \ No newline at end of file diff --git a/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/PlatformImportsTest.java b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/PlatformImportsTest.java new file mode 100644 index 00000000000000..319259c4ba83b4 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/PlatformImportsTest.java @@ -0,0 +1,155 @@ +package io.quarkus.bootstrap.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.quarkus.bootstrap.util.IoUtils; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +public class PlatformImportsTest { + + private final List platformProps = new ArrayList<>(); + + @AfterEach + public void cleanUp() { + for (PlatformProps p : platformProps) { + p.delete(); + } + } + + @Test + public void singlePlatformReleaseInfo() throws Exception { + final PlatformProps props = newPlatformProps(); + props.setProperty("platform.quarkus.native.builder-image", "url"); + props.setRelease(new PlatformReleaseInfo("io.playground", "1.1", "1", + "io.playground:playground-bom::pom:1.1.1,io.playground:acme-bom::pom:2.2.2,io.playground:foo-bom::pom:3.3.3")); + + final PlatformReleases pi = new PlatformReleases(); + props.importRelease(pi); + + final PlatformInfo platform = pi.getPlatform("io.playground"); + assertNotNull(platform); + assertEquals("io.playground", platform.getPlatformKey()); + final PlatformStreamInfo stream = platform.getStream("1.1"); + assertNotNull(stream); + final PlatformReleaseInfo release = stream.getRelease("1"); + assertEquals("io.playground", release.getPlatformKey()); + assertEquals("1.1", release.getStream()); + assertEquals("1", release.getVersion()); + final List boms = release.getBoms(); + assertEquals(Arrays.asList(AppArtifactCoords.fromString("io.playground:playground-bom::pom:1.1.1"), + AppArtifactCoords.fromString("io.playground:acme-bom::pom:2.2.2"), + AppArtifactCoords.fromString("io.playground:foo-bom::pom:3.3.3")), boms); + assertEquals(1, stream.getReleases().size()); + assertEquals(1, platform.getStreams().size()); + assertEquals(1, pi.getPlatforms().size()); + + assertTrue(pi.isAligned(Collections.singletonMap("io.playground", + Arrays.asList(AppArtifactCoords.fromString("io.playground:playground-bom::pom:1.1.1"), + AppArtifactCoords.fromString("io.playground:acme-bom::pom:2.2.2"))))); + assertFalse(pi.isAligned(Collections.singletonMap("io.playground", + Arrays.asList(AppArtifactCoords.fromString("io.playground:playground-bom::pom:1.1.2"), + AppArtifactCoords.fromString("io.playground:acme-bom::pom:2.2.2"))))); + } + + @Test + public void multiplePlatformReleaseInTheSameStream() throws Exception { + final PlatformProps member1 = newPlatformProps(); + member1.setProperty("platform.quarkus.native.builder-image", "url"); + member1.setRelease(new PlatformReleaseInfo("io.playground", "1.1", "1", + "io.playground:playground-bom::pom:1.1.1,io.playground:acme-bom::pom:2.2.2,io.playground:foo-bom::pom:3.3.3")); + + final PlatformProps member2 = newPlatformProps(); + member2.setProperty("platform.quarkus.native.builder-image", "url"); + member2.setRelease(new PlatformReleaseInfo("io.playground", "1.1", "2", + "io.playground:playground-bom::pom:1.1.2,io.playground:acme-bom::pom:2.2.3,io.playground:foo-bom::pom:3.3.3")); + + final PlatformReleases pi = new PlatformReleases(); + member1.importRelease(pi); + member2.importRelease(pi); + + final PlatformInfo platform = pi.getPlatform("io.playground"); + assertNotNull(platform); + assertEquals("io.playground", platform.getPlatformKey()); + final PlatformStreamInfo stream = platform.getStream("1.1"); + assertNotNull(stream); + final PlatformReleaseInfo release = stream.getRelease("1"); + assertEquals("io.playground", release.getPlatformKey()); + assertEquals("1.1", release.getStream()); + assertEquals("1", release.getVersion()); + final List boms = release.getBoms(); + assertEquals(Arrays.asList(AppArtifactCoords.fromString("io.playground:playground-bom::pom:1.1.1"), + AppArtifactCoords.fromString("io.playground:acme-bom::pom:2.2.2"), + AppArtifactCoords.fromString("io.playground:foo-bom::pom:3.3.3")), boms); + assertEquals(2, stream.getReleases().size()); + assertEquals(1, platform.getStreams().size()); + assertEquals(1, pi.getPlatforms().size()); + + assertTrue(pi.isAligned(Collections.singletonMap("io.playground", + Arrays.asList(AppArtifactCoords.fromString("io.playground:playground-bom::pom:1.1.1"), + AppArtifactCoords.fromString("io.playground:acme-bom::pom:2.2.2"))))); + assertTrue(pi.isAligned(Collections.singletonMap("io.playground", + Arrays.asList(AppArtifactCoords.fromString("io.playground:playground-bom::pom:1.1.2"), + AppArtifactCoords.fromString("io.playground:acme-bom::pom:2.2.3"))))); + assertFalse(pi.isAligned(Collections.singletonMap("io.playground", + Arrays.asList(AppArtifactCoords.fromString("io.playground:playground-bom::pom:1.1.2"), + AppArtifactCoords.fromString("io.playground:acme-bom::pom:2.2.2"))))); + } + + private PlatformProps newPlatformProps() throws IOException { + final PlatformProps p = new PlatformProps(); + platformProps.add(p); + return p; + } + + private static class PlatformProps { + + private final Path path; + private Properties props = new Properties(); + + private PlatformProps() throws IOException { + path = Files.createTempFile("quarkus", "platform-imports"); + } + + private void setRelease(PlatformReleaseInfo release) { + props.setProperty(release.getPropertyName(), release.getPropertyValue()); + } + + private void setProperty(String name, String value) { + props.setProperty(name, value); + } + + private void importRelease(PlatformReleases pi) throws IOException { + try (BufferedWriter w = Files.newBufferedWriter(path)) { + props.store(w, "test playground platform props"); + } + props = new Properties(); + try (BufferedReader reader = Files.newBufferedReader(path)) { + props.load(reader); + } + for (Map.Entry prop : props.entrySet()) { + if (PlatformReleases.isPlatformReleaseInfo(prop.getKey().toString())) { + pi.addPlatformRelease(prop.getKey().toString(), prop.getValue().toString()); + } + } + } + + private void delete() { + IoUtils.recursiveDelete(path); + } + } +} diff --git a/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/PlatformInfoTest.java b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/PlatformInfoTest.java new file mode 100644 index 00000000000000..8ba4d4eb73af65 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/PlatformInfoTest.java @@ -0,0 +1,43 @@ +package io.quarkus.bootstrap.model; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +public class PlatformInfoTest { + + @Test + public void emptyIsAligned() throws Exception { + assertTrue(new PlatformInfo("p") + .isAligned(Collections.singletonList(AppArtifactCoords.fromString("io.playground:playground-bom::pom:1.1.1")))); + } + + @Test + public void singleStreamIsAligned() throws Exception { + final PlatformInfo platform = new PlatformInfo("p"); + final PlatformStreamInfo stream = platform.getOrCreateStream("1.1"); + stream.addIfNotPresent("1", () -> new PlatformReleaseInfo("io.playground", "playground-bom", "1.1", + "io.playground:playground-bom::pom:1.1.1,org.acme:acme-bom::pom:2.2.2,com.foo:foo-bom::pom:3.3.3")); + stream.addIfNotPresent("2", () -> new PlatformReleaseInfo("io.playground", "playground-bom", "1.1", + "io.playground:playground-bom::pom:1.1.2,org.acme:acme-bom::pom:2.2.3,com.foo:foo-bom::pom:3.3.3")); + + assertTrue(platform.isAligned(Arrays.asList(AppArtifactCoords.fromString("io.playground:playground-bom::pom:1.1.1"), + AppArtifactCoords.fromString("org.acme:acme-bom::pom:2.2.2")))); + assertTrue(platform.isAligned(Arrays.asList(AppArtifactCoords.fromString("io.playground:playground-bom::pom:1.1.2"), + AppArtifactCoords.fromString("org.acme:acme-bom::pom:2.2.3")))); + assertFalse(platform.isAligned(Arrays.asList(AppArtifactCoords.fromString("io.playground:playground-bom::pom:1.1.2"), + AppArtifactCoords.fromString("org.acme:acme-bom::pom:2.2.2")))); + } + + @Test + public void multipleStreamsAreNotAligned() throws Exception { + final PlatformInfo platform = new PlatformInfo("p"); + platform.getOrCreateStream("1.1"); + platform.getOrCreateStream("1.2"); + assertFalse(platform + .isAligned(Collections.singletonList(AppArtifactCoords.fromString("io.playground:playground-bom::pom:1.1.1")))); + } +} diff --git a/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/PlatformStreamInfoTest.java b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/PlatformStreamInfoTest.java new file mode 100644 index 00000000000000..75e83c763bd0d3 --- /dev/null +++ b/independent-projects/bootstrap/app-model/src/test/java/io/quarkus/bootstrap/model/PlatformStreamInfoTest.java @@ -0,0 +1,32 @@ +package io.quarkus.bootstrap.model; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +public class PlatformStreamInfoTest { + + @Test + public void emptyIsAligned() throws Exception { + assertTrue(new PlatformStreamInfo("1.1") + .isAligned(Collections.singletonList(AppArtifactCoords.fromString("io.playground:playground-bom::pom:1.1.1")))); + } + + @Test + public void isAligned() throws Exception { + final PlatformStreamInfo stream = new PlatformStreamInfo("1.1"); + stream.addIfNotPresent("1", () -> new PlatformReleaseInfo("io.playground", "playground-bom", "1.1", + "io.playground:playground-bom::pom:1.1.1,org.acme:acme-bom::pom:2.2.2,com.foo:foo-bom::pom:3.3.3")); + stream.addIfNotPresent("2", () -> new PlatformReleaseInfo("io.playground", "playground-bom", "1.1", + "io.playground:playground-bom::pom:1.1.2,org.acme:acme-bom::pom:2.2.3,com.foo:foo-bom::pom:3.3.3")); + assertTrue(stream.isAligned(Arrays.asList(AppArtifactCoords.fromString("io.playground:playground-bom::pom:1.1.1"), + AppArtifactCoords.fromString("org.acme:acme-bom::pom:2.2.2")))); + assertTrue(stream.isAligned(Arrays.asList(AppArtifactCoords.fromString("io.playground:playground-bom::pom:1.1.2"), + AppArtifactCoords.fromString("org.acme:acme-bom::pom:2.2.3")))); + assertFalse(stream.isAligned(Arrays.asList(AppArtifactCoords.fromString("io.playground:playground-bom::pom:1.1.2"), + AppArtifactCoords.fromString("org.acme:acme-bom::pom:2.2.2")))); + } +} diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java index 28193461c84acf..c5ac4a96363738 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/BootstrapAppModelResolver.java @@ -3,10 +3,12 @@ import io.quarkus.bootstrap.BootstrapConstants; import io.quarkus.bootstrap.BootstrapDependencyProcessingException; import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.bootstrap.model.AppArtifactCoords; import io.quarkus.bootstrap.model.AppArtifactKey; import io.quarkus.bootstrap.model.AppDependency; import io.quarkus.bootstrap.model.AppModel; import io.quarkus.bootstrap.model.PathsCollection; +import io.quarkus.bootstrap.model.PlatformReleases; import io.quarkus.bootstrap.resolver.maven.BuildDependencyGraphVisitor; import io.quarkus.bootstrap.resolver.maven.DeploymentInjectingDependencyVisitor; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; @@ -17,6 +19,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -268,18 +271,22 @@ private AppModel doResolveModel(AppArtifact appArtifact, List direct private void collectPlatformProperties(AppModel.Builder appBuilder, List managedDeps) throws AppModelResolverException { - final Set descriptorKeys = new HashSet<>(4); - final Set propertyKeys = new HashSet<>(2); + final Set descriptorCoords = new HashSet<>(); + final Set propertyCoords = new HashSet<>(); final Map collectedProps = new HashMap(); + // platform BOMs by platform keys (groupId) + final Map> importedPlatforms = new HashMap<>(); + final PlatformReleases platformReleases = new PlatformReleases(); for (Dependency d : managedDeps) { final Artifact artifact = d.getArtifact(); final String extension = artifact.getExtension(); final String artifactId = artifact.getArtifactId(); if ("json".equals(extension) && artifactId.endsWith(BootstrapConstants.PLATFORM_DESCRIPTOR_ARTIFACT_ID_SUFFIX)) { - descriptorKeys.add(new AppArtifactKey(artifact.getGroupId(), + descriptorCoords.add(new AppArtifactCoords(artifact.getGroupId(), artifactId.substring(0, artifactId.length() - BootstrapConstants.PLATFORM_DESCRIPTOR_ARTIFACT_ID_SUFFIX.length()), + null, "pom", artifact.getVersion())); } else if ("properties".equals(artifact.getExtension()) && artifactId.endsWith(BootstrapConstants.PLATFORM_PROPERTIES_ARTIFACT_ID_SUFFIX)) { @@ -290,25 +297,31 @@ private void collectPlatformProperties(AppModel.Builder appBuilder, List prop : props.entrySet()) { final String name = String.valueOf(prop.getKey()); if (name.startsWith(BootstrapConstants.PLATFORM_PROPERTY_PREFIX)) { - collectedProps.putIfAbsent(prop.getKey().toString(), prop.getValue().toString()); + if (PlatformReleases.isPlatformReleaseInfo(name)) { + platformReleases.addPlatformRelease(name, String.valueOf(prop.getValue())); + } else { + collectedProps.putIfAbsent(name, String.valueOf(prop.getValue().toString())); + } } } - propertyKeys.add(new AppArtifactKey(artifact.getGroupId(), - artifactId.substring(0, - artifactId.length() - BootstrapConstants.PLATFORM_PROPERTIES_ARTIFACT_ID_SUFFIX.length()), - artifact.getVersion())); + final AppArtifactCoords bomCoords = new AppArtifactCoords(artifact.getGroupId(), bomArtifactId, null, "pom", + artifact.getVersion()); + importedPlatforms.computeIfAbsent(artifact.getGroupId(), g -> new ArrayList<>()).add(bomCoords); + propertyCoords.add(bomCoords); } } - if (!descriptorKeys.containsAll(propertyKeys)) { + if (!descriptorCoords.containsAll(propertyCoords)) { final StringBuilder buf = new StringBuilder(); buf.append( "The Quarkus platform properties applied to the project are missing the corresponding Quarkus platform BOM imports:"); final int l = buf.length(); - for (AppArtifactKey key : propertyKeys) { - if (!descriptorKeys.contains(key)) { + for (AppArtifactCoords key : propertyCoords) { + if (!descriptorCoords.contains(key)) { if (l - buf.length() < 0) { buf.append(','); } @@ -317,6 +330,8 @@ private void collectPlatformProperties(AppModel.Builder appBuilder, Listpom import + {#each boms} + + {it.groupId} + {it.artifactId} + {it.version} + pom + import + + {/each} {#if maven.repositories} diff --git a/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/CodestartProjectInput.java b/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/CodestartProjectInput.java index 957e672f188817..f1ce7a62fc8fa9 100644 --- a/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/CodestartProjectInput.java +++ b/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/CodestartProjectInput.java @@ -9,12 +9,14 @@ public class CodestartProjectInput { private final Collection dependencies; + private final Collection boms; private final Map data; private final CodestartsSelection selection; private final MessageWriter messageWriter; protected CodestartProjectInput(final CodestartProjectInputBuilder builder) { this.dependencies = requireNonNull(builder.dependencies, "dependencies is required"); + this.boms = requireNonNull(builder.boms, "boms is required"); this.selection = requireNonNull(builder.selection, "selection is required"); this.data = NestedMaps.unflatten(requireNonNull(builder.data, "data is required")); this.messageWriter = requireNonNull(builder.messageWriter, "messageWriter is required"); @@ -36,6 +38,10 @@ public Collection getDependencies() { return dependencies; } + public Collection getBoms() { + return boms; + } + public Map getData() { return data; } diff --git a/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/CodestartProjectInputBuilder.java b/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/CodestartProjectInputBuilder.java index 3045b761c010da..0dea8726973a3c 100644 --- a/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/CodestartProjectInputBuilder.java +++ b/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/CodestartProjectInputBuilder.java @@ -9,6 +9,7 @@ public class CodestartProjectInputBuilder { Collection dependencies = new ArrayList<>(); + Collection boms = new ArrayList<>(); CodestartsSelection selection = new CodestartsSelection(); Map data = new HashMap<>(); MessageWriter messageWriter = MessageWriter.info(); @@ -26,6 +27,11 @@ public CodestartProjectInputBuilder addDependency(String dependency) { return this.addDependencies(Collections.singletonList(dependency)); } + public CodestartProjectInputBuilder addBoms(Collection boms) { + this.boms.addAll(boms); + return this; + } + public CodestartProjectInputBuilder addCodestarts(Collection codestarts) { this.selection.addNames(codestarts); return this; diff --git a/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/CodestartData.java b/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/CodestartData.java index 1936afe2f28d2a..0c54bb9c66d67c 100644 --- a/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/CodestartData.java +++ b/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/CodestartData.java @@ -54,8 +54,11 @@ public static Map buildCodestartProjectData(Collection buildDependenciesData(Stream codestartsStream, String languageName, - Collection extensions) { + Collection extensions, Collection platforms) { final Map> depsData = new HashMap<>(); + final Set boms = platforms.stream() + .map(CodestartDep::new) + .collect(Collectors.toCollection(LinkedHashSet::new)); final Set dependencies = extensions.stream() .map(CodestartDep::new) .collect(Collectors.toCollection(LinkedHashSet::new)); @@ -67,6 +70,7 @@ public static Map buildDependenciesData(Stream codest testDependencies.addAll(d.getTestDependencies()); }); depsData.put("dependencies", dependencies); + depsData.put("boms", boms); depsData.put("test-dependencies", testDependencies); return Collections.unmodifiableMap(depsData); } diff --git a/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/CodestartSpec.java b/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/CodestartSpec.java index 4b696ead7df86f..0ac17259504ce6 100644 --- a/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/CodestartSpec.java +++ b/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/CodestartSpec.java @@ -85,20 +85,23 @@ public static final class LanguageSpec { private final Map data; private final Map sharedData; private final List dependencies; + private final List boms; private final List testDependencies; public LanguageSpec() { - this(null, null, null, null); + this(null, null, null, null, null); } @JsonCreator public LanguageSpec(@JsonProperty("data") Map data, @JsonProperty("shared-data") Map sharedData, @JsonProperty("dependencies") List dependencies, + @JsonProperty("boms") List boms, @JsonProperty("test-dependencies") List testDependencies) { this.data = data != null ? data : Collections.emptyMap(); this.sharedData = sharedData != null ? sharedData : Collections.emptyMap(); this.dependencies = dependencies != null ? dependencies : Collections.emptyList(); + this.boms = boms != null ? boms : Collections.emptyList(); this.testDependencies = testDependencies != null ? testDependencies : Collections.emptyList(); } @@ -114,6 +117,10 @@ public List getDependencies() { return dependencies; } + public List getBoms() { + return boms; + } + public List getTestDependencies() { return testDependencies; } diff --git a/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/DefaultCodestartProjectDefinition.java b/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/DefaultCodestartProjectDefinition.java index 97815c134cb57d..faa1914925ce94 100644 --- a/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/DefaultCodestartProjectDefinition.java +++ b/independent-projects/tools/codestarts/src/main/java/io/quarkus/devtools/codestarts/core/DefaultCodestartProjectDefinition.java @@ -91,7 +91,7 @@ public Map getSharedData() { @Override public Map getDepsData() { return buildDependenciesData(getCodestarts().stream(), getLanguageName(), - getProjectInput().getDependencies()); + getProjectInput().getDependencies(), getProjectInput().getBoms()); } @Override diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartProjectInput.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartProjectInput.java index 90f54683aea726..8326b9870dbab5 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartProjectInput.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartProjectInput.java @@ -12,12 +12,14 @@ public final class QuarkusCodestartProjectInput extends CodestartProjectInput { private final BuildTool buildTool; private final Collection extensions; + private final Collection platforms; private final String example; private Set appContent; public QuarkusCodestartProjectInput(QuarkusCodestartProjectInputBuilder builder) { super(builder); this.extensions = builder.extensions; + this.platforms = builder.platforms; this.example = builder.example; this.buildTool = requireNonNull(builder.buildTool, "buildTool is required"); this.appContent = builder.appContent; @@ -31,6 +33,10 @@ public Collection getExtensions() { return extensions; } + public Collection getPlatforms() { + return platforms; + } + public String getExample() { return example; } diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartProjectInputBuilder.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartProjectInputBuilder.java index 00ca78c79c20c8..898e31c658b663 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartProjectInputBuilder.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/codestarts/quarkus/QuarkusCodestartProjectInputBuilder.java @@ -22,6 +22,7 @@ public class QuarkusCodestartProjectInputBuilder extends CodestartProjectInputBu private static final List FULL_CONTENT = Arrays.asList(AppContent.values()); Collection extensions = new ArrayList<>(); + Collection platforms = new ArrayList<>(); Set appContent = new HashSet<>(FULL_CONTENT); String example; BuildTool buildTool = BuildTool.MAVEN; @@ -44,6 +45,12 @@ public QuarkusCodestartProjectInputBuilder addExtension(ArtifactKey extension) { return this.addExtension(Extensions.toCoords(extension, null)); } + public QuarkusCodestartProjectInputBuilder addPlatforms(Collection boms) { + this.platforms.addAll(boms); + super.addBoms(boms.stream().map(Extensions::toGAV).collect(Collectors.toList())); + return this; + } + public QuarkusCodestartProjectInputBuilder example(String example) { this.example = example; return this; 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 c5e4775ab559ac..8d202ad7175f66 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 @@ -27,10 +27,14 @@ import io.quarkus.devtools.project.codegen.ProjectGenerator; import io.quarkus.maven.ArtifactCoords; import io.quarkus.platform.tools.ToolsUtils; +import io.quarkus.registry.catalog.Extension; import io.quarkus.registry.catalog.ExtensionCatalog; +import io.quarkus.registry.union.ElementCatalog; +import io.quarkus.registry.union.ElementCatalogBuilder; import java.io.IOException; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; @@ -45,18 +49,10 @@ public class CreateProjectCommandHandler implements QuarkusCommandHandler { @Override public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws QuarkusCommandException { - final ExtensionCatalog platformDescr = invocation.getExtensionsCatalog(); - final ArtifactCoords bom = platformDescr.getBom(); - if (bom == null) { - throw new QuarkusCommandException("The platform BOM is missing"); - } - invocation.setValue(BOM_GROUP_ID, bom.getGroupId()); - invocation.setValue(BOM_ARTIFACT_ID, bom.getArtifactId()); - invocation.setValue(BOM_VERSION, bom.getVersion()); - invocation.setValue(QUARKUS_VERSION, platformDescr.getQuarkusCoreVersion()); + final ExtensionCatalog extensionCatalog = invocation.getExtensionsCatalog(); final Set extensionsQuery = invocation.getValue(ProjectGenerator.EXTENSIONS, Collections.emptySet()); - final Properties quarkusProps = ToolsUtils.readQuarkusProperties(platformDescr); + final Properties quarkusProps = ToolsUtils.readQuarkusProperties(extensionCatalog); quarkusProps.forEach((k, v) -> { String name = k.toString().replace("-", "_"); if (!invocation.hasValue(name)) { @@ -82,15 +78,61 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws if (extensionsToAdd == null) { throw new QuarkusCommandException("Failed to create project because of invalid extensions"); } + + List boms = Collections.emptyList(); + ArtifactCoords bom = null; + final ElementCatalog ec = (ElementCatalog) extensionCatalog.getMetadata().get("element-catalog"); + if (ec != null) { + // we add quarkus-core as a selected extension here only to include the quarkus-bom + // in the list of platforms. quarkus-core won't be added to the generated POM though. + final Extension quarkusCore = invocation.getExtensionsCatalog().getExtensions().stream() + .filter(e -> e.getArtifact().getArtifactId().equals("quarkus-core")).findFirst().get(); + if (quarkusCore == null) { + throw new QuarkusCommandException("Failed to locate quarkus-core in the extension catalog"); + } + final List eKeys; + if (extensionsToAdd.isEmpty()) { + eKeys = Collections.singletonList( + quarkusCore.getArtifact().getGroupId() + ":" + quarkusCore.getArtifact().getArtifactId()); + } else { + eKeys = extensionsToAdd.stream().map(e -> e.getGroupId() + ":" + e.getArtifactId()) + .collect(Collectors.toList()); + eKeys.add(quarkusCore.getArtifact().getGroupId() + ":" + quarkusCore.getArtifact().getArtifactId()); + } + boms = ElementCatalogBuilder.getBoms(ec, eKeys); + final Iterator i = boms.iterator(); + while (i.hasNext()) { + final ArtifactCoords next = i.next(); + // TODO we remove quarkus-bom here because it's currently added by default using properties in the template + // ideally, it shouldn't be different than the rest of the imported BOMs + if (next.getArtifactId().equals("quarkus-bom")) { + bom = next; + i.remove(); + break; + } + } + } else { + bom = extensionCatalog.getBom(); + } + if (bom == null) { + throw new QuarkusCommandException("The platform BOM is missing"); + } + + invocation.setValue(BOM_GROUP_ID, bom.getGroupId()); + invocation.setValue(BOM_ARTIFACT_ID, bom.getArtifactId()); + invocation.setValue(BOM_VERSION, bom.getVersion()); + invocation.setValue(QUARKUS_VERSION, extensionCatalog.getQuarkusCoreVersion()); + try { Map platformData = new HashMap<>(); - if (platformDescr.getMetadata().get("maven") != null) { - platformData.put("maven", platformDescr.getMetadata().get("maven")); + if (extensionCatalog.getMetadata().get("maven") != null) { + platformData.put("maven", extensionCatalog.getMetadata().get("maven")); } - if (platformDescr.getMetadata().get("gradle") != null) { - platformData.put("gradle", platformDescr.getMetadata().get("gradle")); + if (extensionCatalog.getMetadata().get("gradle") != null) { + platformData.put("gradle", extensionCatalog.getMetadata().get("gradle")); } final QuarkusCodestartProjectInput input = QuarkusCodestartProjectInput.builder() + .addPlatforms(boms) .addExtensions(extensionsToAdd) .buildTool(invocation.getQuarkusProject().getBuildTool()) .example(invocation.getValue(EXAMPLE)) @@ -124,6 +166,7 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws } catch (IOException e) { throw new QuarkusCommandException("Failed to create project: " + e.getMessage(), e); } + return QuarkusCommandOutcome.success(); } } diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/QuarkusProjectHelper.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/QuarkusProjectHelper.java index e0bb7bd65430c7..35226d0336e96b 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/QuarkusProjectHelper.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/QuarkusProjectHelper.java @@ -52,9 +52,13 @@ public static ExtensionCatalog getExtensionCatalog(String quarkusVersion) { // TODO remove this method once the default registry becomes available final ExtensionCatalogResolver catalogResolver = getCatalogResolver(); try { - return catalogResolver.hasRegistries() ? catalogResolver.resolveExtensionCatalog(quarkusVersion) - : ToolsUtils.resolvePlatformDescriptorDirectly(null, null, quarkusVersion, artifactResolver(), - messageWriter()); + if (catalogResolver.hasRegistries()) { + return quarkusVersion == null ? catalogResolver.resolveExtensionCatalog() + : catalogResolver.resolveExtensionCatalog(quarkusVersion); + } else { + return ToolsUtils.resolvePlatformDescriptorDirectly(null, null, quarkusVersion, artifactResolver(), + messageWriter()); + } } catch (Exception e) { throw new RuntimeException("Failed to resolve the Quarkus extension catalog", e); } diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/AbstractGroovyGradleBuildFile.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/AbstractGroovyGradleBuildFile.java index aa891c0f3fc8f7..17fbb6ff1ffd8d 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/AbstractGroovyGradleBuildFile.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/AbstractGroovyGradleBuildFile.java @@ -29,6 +29,11 @@ String getBuildGradlePath() { return BUILD_GRADLE_PATH; } + @Override + protected boolean importBom(ArtifactCoords coords) { + return importBomInModel(getModel(), coords); + } + @Override protected boolean addDependency(ArtifactCoords coords, boolean managed) { return addDependencyInModel(getModel(), coords, managed); @@ -39,6 +44,12 @@ public BuildTool getBuildTool() { return BuildTool.GRADLE; } + static boolean importBomInModel(Model model, ArtifactCoords coords) { + return addDependencyInModel(model, + String.format(" implementation enforcedPlatform(%s)%n", + createDependencyCoordinatesString(coords, false, '\''))); + } + static boolean addDependencyInModel(Model model, ArtifactCoords coords, boolean managed) { return addDependencyInModel(model, String.format(" implementation %s%n", createDependencyCoordinatesString(coords, managed, '\''))); diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/BuildFile.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/BuildFile.java index 019692b474be76..c950a6c869142b 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/BuildFile.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/BuildFile.java @@ -53,7 +53,7 @@ public InstallResult install(ExtensionInstallPlan plan) throws IOException { List installedPlatforms = new ArrayList<>(); final Set alreadyInstalled = alreadyInstalled(plan.toCollection()); for (ArtifactCoords platform : withoutAlreadyInstalled(alreadyInstalled, plan.getPlatforms())) { - if (addDependency(platform, false)) { + if (importBom(platform)) { installedPlatforms.add(platform); } } @@ -116,6 +116,8 @@ private Collection withoutAlreadyInstalled(Set exis .collect(toList()); } + protected abstract boolean importBom(ArtifactCoords coords); + protected abstract boolean addDependency(ArtifactCoords coords, boolean managed); protected abstract void removeDependency(ArtifactKey key) throws IOException; diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/MavenBuildFile.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/MavenBuildFile.java index 6c5e33ac9ac824..358b2e2968d842 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/MavenBuildFile.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/MavenBuildFile.java @@ -49,6 +49,32 @@ public void writeToDisk() throws IOException { } } + @Override + protected boolean importBom(ArtifactCoords coords) { + if (!"pom".equalsIgnoreCase(coords.getType())) { + throw new IllegalArgumentException(coords + " is not a POM"); + } + Model model = getModel(); + final Dependency d = new Dependency(); + d.setGroupId(coords.getGroupId()); + d.setArtifactId(coords.getArtifactId()); + d.setType(coords.getType()); + d.setScope("import"); + DependencyManagement dependencyManagement = model.getDependencyManagement(); + if (dependencyManagement == null) { + dependencyManagement = new DependencyManagement(); + model.setDependencyManagement(dependencyManagement); + } + if (dependencyManagement.getDependencies() + .stream() + .map(this::toResolvedDependency) + .noneMatch(thisDep -> d.getManagementKey().equals(thisDep.getManagementKey()))) { + dependencyManagement.addDependency(d); + return true; + } + return false; + } + @Override protected boolean addDependency(ArtifactCoords coords, boolean managed) { Model model = getModel(); diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/MavenProjectBuildFile.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/MavenProjectBuildFile.java index 66383ce637f218..9baf838d7f5528 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/MavenProjectBuildFile.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/MavenProjectBuildFile.java @@ -6,6 +6,7 @@ import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject; +import io.quarkus.bootstrap.resolver.maven.workspace.ModelUtils; import io.quarkus.devtools.messagewriter.MessageWriter; import io.quarkus.devtools.project.BuildTool; import io.quarkus.devtools.project.QuarkusProject; @@ -84,7 +85,8 @@ public static QuarkusProject getProject(Artifact projectPom, Model projectModel, final ExtensionCatalogResolver catalogResolver = QuarkusProjectHelper.getCatalogResolver(mvnResolver, log); if (catalogResolver.hasRegistries()) { try { - extensionCatalog = catalogResolver.resolveExtensionCatalog(quarkusVersion); + //extensionCatalog = catalogResolver.resolveExtensionCatalog(quarkusVersion); + extensionCatalog = catalogResolver.resolveExtensionCatalog(); } catch (RegistryResolutionException e) { throw new RuntimeException("Failed to resolve extension catalog", e); } @@ -165,9 +167,9 @@ private static boolean isSameFile(Path p1, Path p2) { } } - private final Model model; - private final List managedDependencies; - private final Properties projectProps; + private Model model; + private List managedDependencies; + private Properties projectProps; private Supplier> projectDepsSupplier; private List dependencies; private List importedPlatforms; @@ -188,6 +190,36 @@ public BuildTool getBuildTool() { return BuildTool.MAVEN; } + @Override + protected boolean importBom(ArtifactCoords coords) { + if (!"pom".equalsIgnoreCase(coords.getType())) { + throw new IllegalArgumentException(coords + " is not a POM"); + } + final Dependency d = new Dependency(); + d.setGroupId(coords.getGroupId()); + d.setArtifactId(coords.getArtifactId()); + d.setType(coords.getType()); + d.setScope("import"); + d.setVersion(coords.getVersion()); + DependencyManagement dependencyManagement = model().getDependencyManagement(); + if (dependencyManagement == null) { + dependencyManagement = new DependencyManagement(); + model().setDependencyManagement(dependencyManagement); + } + if (dependencyManagement.getDependencies() + .stream() + .filter(t -> t.getScope().equals("import")) + .noneMatch(thisDep -> d.getManagementKey().equals(resolveKey(thisDep)))) { + dependencyManagement.addDependency(d); + // the effective managed dependencies set may already include it + if (!getManagedDependencies().contains(coords)) { + getManagedDependencies().add(coords); + } + return true; + } + return false; + } + @Override protected boolean addDependency(ArtifactCoords coords, boolean managed) { final Dependency d = new Dependency(); @@ -297,6 +329,21 @@ protected String getProperty(String propertyName) { @Override protected void refreshData() { + final Path projectPom = getProjectDirPath().resolve("pom.xml"); + if (Files.exists(projectPom)) { + try { + model = ModelUtils.readModel(projectPom); + } catch (IOException e) { + throw new RuntimeException("Failed to read " + projectPom, e); + } + projectProps = model.getProperties(); + final ArtifactDescriptorResult descriptor = describe(getMavenResolver(getProjectDirPath()), new DefaultArtifact( + ModelUtils.getGroupId(model), model.getArtifactId(), "pom", ModelUtils.getVersion(model))); + managedDependencies = toArtifactCoords(descriptor.getManagedDependencies()); + dependencies = null; + projectDepsSupplier = () -> toArtifactCoords(descriptor.getDependencies()); + + } } private int getIndexToAddExtension() { diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/ExtensionCatalogResolver.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/ExtensionCatalogResolver.java index 09d4d672482a59..e3ac2da89374a1 100644 --- a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/ExtensionCatalogResolver.java +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/ExtensionCatalogResolver.java @@ -17,6 +17,11 @@ import io.quarkus.registry.config.RegistriesConfig; import io.quarkus.registry.config.RegistriesConfigLocator; import io.quarkus.registry.config.RegistryConfig; +import io.quarkus.registry.union.ElementCatalog; +import io.quarkus.registry.union.ElementCatalogBuilder; +import io.quarkus.registry.union.ElementCatalogBuilder.MemberBuilder; +import io.quarkus.registry.union.ElementCatalogBuilder.UnionBuilder; +import io.quarkus.registry.util.PlatformArtifacts; import java.io.File; import java.net.URL; import java.net.URLClassLoader; @@ -250,8 +255,75 @@ private void collectPlatforms(PlatformCatalog catalog, List collectedP } } + @SuppressWarnings("unchecked") public ExtensionCatalog resolveExtensionCatalog() throws RegistryResolutionException { - return resolveExtensionCatalog((String) null); + + final int registriesTotal = registries.size(); + if (registriesTotal == 0) { + throw new RegistryResolutionException("No registries configured"); + } + + final Set processedUnions = new HashSet<>(); + final List psList = new ArrayList<>(); + final Map platformDescrMap = new HashMap<>(); + final List catalogs = new ArrayList<>(); + final ElementCatalogBuilder catalogBuilder = ElementCatalogBuilder.newInstance(); + + for (RegistryExtensionResolver registry : registries) { + final PlatformCatalog pc = registry.resolvePlatformCatalog(); + if (pc == null) { + continue; + } + for (Platform p : pc.getPlatforms()) { + final ExtensionCatalog ec = registry.resolvePlatformExtensions(p.getBom()); + catalogs.add(ec); + platformDescrMap.put(ec.getBom().getGroupId() + ":" + ec.getBom().getArtifactId(), ec); + + final Map platformRelease = (Map) ec.getMetadata().get("platform-release"); + if (platformRelease != null) { + final String versionStr = (String) platformRelease.get("version"); + if (!processedUnions.add(versionStr)) { + continue; + } + final UnionBuilder union = catalogBuilder.getOrCreateUnion(Integer.parseInt(versionStr)); + psList.add(new ParsedPlatformStack(union, ec.getId(), (List) platformRelease.get("members"))); + addMember(union, ec); + } + } + } + + for (ParsedPlatformStack stack : psList) { + for (String memberCoordsStr : stack.members) { + if (stack.originMemberId.equals(memberCoordsStr)) { + continue; + } + final ArtifactCoords memberCoords = ArtifactCoords.fromString(memberCoordsStr); + ExtensionCatalog memberCatalog = platformDescrMap + .get(memberCoords.getGroupId() + ":" + + PlatformArtifacts.ensureBomArtifactId(memberCoords.getArtifactId())); + if (memberCatalog == null || !memberCatalog.getBom().getVersion().equals(memberCoords.getVersion())) { + memberCatalog = registries.get(0).resolvePlatformExtensions(memberCoords); + } + + if (memberCatalog != null) { + addMember(stack.unionBuilder, memberCatalog); + } + } + } + + final ExtensionCatalog catalog = JsonCatalogMerger.merge(catalogs); + final ElementCatalog elements = catalogBuilder.build(); + if (!elements.isEmpty()) { + catalog.getMetadata().put("element-catalog", elements); + } + return catalog; + } + + private static void addMember(final UnionBuilder union, ExtensionCatalog member) { + final MemberBuilder builder = union.getOrCreateMember( + member.getBom().getGroupId() + ":" + member.getBom().getArtifactId(), member.getBom().getVersion()); + member.getExtensions() + .forEach(e -> builder.addElement(e.getArtifact().getGroupId() + ":" + e.getArtifact().getArtifactId())); } public ExtensionCatalog resolveExtensionCatalog(String quarkusCoreVersion) throws RegistryResolutionException { @@ -499,4 +571,16 @@ private List filterRegistries(Function members; + + public ParsedPlatformStack(UnionBuilder ub, String originMemberId, List members) { + this.unionBuilder = ub; + this.originMemberId = originMemberId; + this.members = members; + } + } } diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/Element.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/Element.java new file mode 100644 index 00000000000000..b709dac8b9483b --- /dev/null +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/Element.java @@ -0,0 +1,20 @@ +package io.quarkus.registry.union; + +import java.util.Collection; + +public interface Element { + + /** + * Element key. + * + * @return element key + */ + Object key(); + + /** + * Members that provide the element. + * + * @return members that provide the element + */ + Collection members(); +} diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/ElementCatalog.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/ElementCatalog.java new file mode 100644 index 00000000000000..442ed6fcbf7a77 --- /dev/null +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/ElementCatalog.java @@ -0,0 +1,35 @@ +package io.quarkus.registry.union; + +import java.util.Collection; + +public interface ElementCatalog { + + /** + * All elements of the catalog + * + * @return elements of the catalog + */ + Collection elements(); + + /** + * All element keys + * + * @return all element keys + */ + Collection elementKeys(); + + /** + * Returns an element for a given key. + * + * @param elementKey element key + * @return element associated with the key or null + */ + Element get(Object elementKey); + + /** + * Checks whether the catalog contains any elements. + * + * @return true if the catalog does not contain any elements, otherwise - false + */ + boolean isEmpty(); +} diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/ElementCatalogBuilder.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/ElementCatalogBuilder.java new file mode 100644 index 00000000000000..56ebda35b87ef3 --- /dev/null +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/ElementCatalogBuilder.java @@ -0,0 +1,361 @@ +package io.quarkus.registry.union; + +import io.quarkus.maven.ArtifactCoords; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; + +public class ElementCatalogBuilder { + + public static ElementCatalogBuilder newInstance() { + return new ElementCatalogBuilder(); + } + + public class ElementBuilder extends BuildCallback { + + private final Object key; + private final Object version; + private final List> callbacks = new ArrayList<>(4); + private final List members = new ArrayList<>(); + + private ElementBuilder(Object key, Object version) { + this.key = Objects.requireNonNull(key); + this.version = Objects.requireNonNull(version); + } + + private ElementBuilder addCallback(MemberBuilder callback) { + callbacks.add(callback); + callback.callbacks.add(this); + return this; + } + + private Element build() { + final Element e = new Element() { + @Override + public Object key() { + return key; + } + + @Override + public Collection members() { + return members; + } + + @Override + public String toString() { + return key.toString() + "#" + version; + } + }; + callbacks.forEach(c -> c.created(e)); + return e; + } + + @Override + protected void created(Member t) { + members.add(t); + } + } + + public class MemberBuilder extends BuildCallback { + private final Object key; + private final Object version; + private Union initialUnion; + private List unionVersions = new ArrayList<>(); + private final List> callbacks = new ArrayList<>(); + private final Map elements = new HashMap<>(); + + private MemberBuilder(Object key, Object version) { + this.key = Objects.requireNonNull(key); + this.version = Objects.requireNonNull(version); + } + + public ElementBuilder addElement(Object elementKey) { + return getOrCreateElement(elementKey, version).addCallback(this); + } + + MemberBuilder addUnion(UnionBuilder union) { + callbacks.add(union); + unionVersions.add(union.version); + return this; + } + + @Override + protected void created(Element t) { + elements.put(t.key(), t); + } + + public Member build() { + final Member m = new Member() { + + @Override + public Object key() { + return key; + } + + @Override + public Object version() { + return version; + } + + @Override + public Union initialUnion() { + return initialUnion; + } + + @Override + public Collection elements() { + return elements.values(); + } + + @Override + public Collection elementKeys() { + return elements.keySet(); + } + + @Override + public Element get(Object elementKey) { + return elements.get(elementKey); + } + + @Override + public String toString() { + return key.toString() + "#" + version + elements.values(); + } + + @Override + public boolean containsAll(Collection elementKeys) { + return elements.keySet().containsAll(elementKeys); + } + + @Override + public Collection unions() { + return unionVersions; + } + + @Override + public boolean isEmpty() { + return elements.isEmpty(); + } + }; + callbacks.forEach(c -> c.created(m)); + return m; + } + } + + public class UnionBuilder extends BuildCallback { + + private final UnionVersion version; + private final List memberBuilders = new ArrayList<>(); + private final Map members = new HashMap<>(); + + private UnionBuilder(UnionVersion version) { + this.version = Objects.requireNonNull(version); + } + + public MemberBuilder getOrCreateMember(Object memberKey, Object memberVersion) { + final MemberBuilder mb = ElementCatalogBuilder.this.getOrCreateMember(memberKey, memberVersion); + memberBuilders.add(mb); + return mb.addUnion(this); + } + + @Override + protected void created(Member t) { + members.put(t.key(), t); + } + + public Union build() { + final Union u = new Union() { + + @Override + public UnionVersion verion() { + return version; + } + + @Override + public Collection members() { + return members.values(); + } + + @Override + public Member member(Object memberKey) { + return members.get(memberKey); + } + + @Override + public String toString() { + return version.toString() + members; + } + }; + for (MemberBuilder mb : memberBuilders) { + mb.initialUnion = u; + } + return u; + } + } + + private abstract class BuildCallback { + + protected abstract void created(T t); + } + + static class IntVersion implements UnionVersion { + + static UnionVersion get(Integer i) { + return new IntVersion(i); + } + + private final Integer version; + + public IntVersion(int version) { + this.version = version; + } + + @Override + public int compareTo(UnionVersion o) { + if (o instanceof IntVersion) { + return version.compareTo(((IntVersion) o).version); + } + throw new IllegalArgumentException(o + " is not an instance of " + IntVersion.class.getName()); + } + + @Override + public String toString() { + return version.toString(); + } + } + + private final Map elements = new HashMap<>(); + private final Map members = new HashMap<>(); + private final Map unions = new HashMap<>(); + + private ElementBuilder getOrCreateElement(Object elementKey, Object elementVersion) { + return elements.computeIfAbsent(elementKey, k -> new ElementBuilder(k, elementVersion)); + } + + private MemberBuilder getOrCreateMember(Object key, Object version) { + return members.computeIfAbsent(key + ":" + version, k -> new MemberBuilder(key, version)); + } + + public UnionBuilder getOrCreateUnion(int version) { + return getOrCreateUnion(IntVersion.get(version)); + } + + public UnionBuilder getOrCreateUnion(UnionVersion version) { + return unions.computeIfAbsent(version, v -> new UnionBuilder(version)); + } + + public ElementCatalog build() { + + final Map map = new HashMap<>(elements.size()); + for (ElementBuilder eb : elements.values()) { + final Element e = eb.build(); + map.put(e.key(), e); + } + for (MemberBuilder m : members.values()) { + m.build(); + } + for (UnionBuilder u : unions.values()) { + u.build(); + } + + final ElementCatalog catalog = new ElementCatalog() { + + @Override + public Collection elements() { + return map.values(); + } + + @Override + public Collection elementKeys() { + return elements.keySet(); + } + + @Override + public Element get(Object elementKey) { + return map.get(elementKey); + } + + @Override + public String toString() { + return elements.toString(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + }; + + return catalog; + + } + + public static void dump(PrintStream ps, ElementCatalog catalog) { + ps.println("Element Catalog:"); + final Map> unions = new TreeMap<>(); + for (Element e : catalog.elements()) { + for (Member m : e.members()) { + for (UnionVersion uv : m.unions()) { + unions.computeIfAbsent(uv, v -> new HashMap<>()).put(m.key(), m); + } + } + } + for (Map.Entry> entry : unions.entrySet()) { + System.out.println("Union " + entry.getKey()); + for (Member m : entry.getValue().values()) { + System.out.println(" Member " + m.key() + ":" + m.version()); + for (Object e : m.elementKeys()) { + System.out.println(" Element " + e); + } + } + } + } + + public static List getBoms(ElementCatalog elementCatalog, Collection elementKeys) { + + final Comparator comparator = UnionVersion::compareTo; + final Map> unionVersions = new TreeMap<>(comparator.reversed()); + for (Object elementKey : elementKeys) { + final Element e = elementCatalog.get(elementKey); + if (e == null) { + throw new RuntimeException( + "Element " + elementKey + " not found in the catalog " + elementCatalog.elementKeys()); + } + for (Member m : e.members()) { + for (UnionVersion uv : m.unions()) { + unionVersions.computeIfAbsent(uv, v -> new HashMap<>()) + .put(ArtifactCoords.fromString(m.key() + "::pom:" + m.version()), m); + } + } + } + + for (Map members : unionVersions.values()) { + final Set memberElementKeys = new HashSet<>(); + final Iterator i = members.values().iterator(); + Member m = null; + while (i.hasNext()) { + m = i.next(); + memberElementKeys.addAll(m.elementKeys()); + } + if (memberElementKeys.containsAll(elementKeys)) { + final List boms = new ArrayList<>(); + for (ArtifactCoords bom : members.keySet()) { + boms.add(bom); + } + return boms; + } + } + return Collections.emptyList(); + } + +} diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/Member.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/Member.java new file mode 100644 index 00000000000000..dd9d9da21ae410 --- /dev/null +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/Member.java @@ -0,0 +1,42 @@ +package io.quarkus.registry.union; + +import java.util.Collection; + +public interface Member extends ElementCatalog { + + /** + * Member key + * + * @return member key + */ + Object key(); + + /** + * Member version + * + * @return member version + */ + Object version(); + + /** + * The very first union the member joined. + * + * @return the very first union the member joined the union + */ + Union initialUnion(); + + /** + * Unions this member belongs to. + * + * @return unions this member belongs to + */ + Collection unions(); + + /** + * Checks whether this member contains all the element keys. + * + * @param elementKeys element keys + * @return true if the member contains all the element keys, otherwise - false + */ + boolean containsAll(Collection elementKeys); +} diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/Playground.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/Playground.java new file mode 100644 index 00000000000000..67b686886ced56 --- /dev/null +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/Playground.java @@ -0,0 +1,110 @@ +package io.quarkus.registry.union; + +import io.quarkus.registry.union.ElementCatalogBuilder.MemberBuilder; +import io.quarkus.registry.union.ElementCatalogBuilder.UnionBuilder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +public class Playground { + + public static void main(String[] args) throws Exception { + + final ElementCatalogBuilder catalogBuilder = ElementCatalogBuilder.newInstance(); + + UnionBuilder unionBuilder = catalogBuilder.getOrCreateUnion(1); + MemberBuilder kogito = unionBuilder.getOrCreateMember("kogito", "1.1.1"); + kogito.addElement("kogito-e1"); + kogito.addElement("kogito-e2"); + + MemberBuilder camel = unionBuilder.getOrCreateMember("camel", "1.2.2"); + camel.addElement("camel-e1"); + camel.addElement("camel-e2"); + + unionBuilder = catalogBuilder.getOrCreateUnion(2); + kogito = unionBuilder.getOrCreateMember("kogito", "2.1.1"); + kogito.addElement("kogito-e1"); + kogito.addElement("kogito-e2"); + + camel = unionBuilder.getOrCreateMember("camel", "2.2.2"); + camel.addElement("camel-e1"); + camel.addElement("camel-e2"); + + unionBuilder = catalogBuilder.getOrCreateUnion(3); + kogito = unionBuilder.getOrCreateMember("kogito", "3.1.1"); + kogito.addElement("kogito-e1"); + kogito.addElement("kogito-e2"); + + unionBuilder = catalogBuilder.getOrCreateUnion(4); + kogito = unionBuilder.getOrCreateMember("kogito", "4.1.1"); + kogito.addElement("kogito-e1"); + kogito.addElement("kogito-e2"); + + final ElementCatalog catalog = catalogBuilder.build(); + + log(catalog); + + List members = membersFor(catalog, "kogito-e2"); + + final StringBuilder buf = new StringBuilder(); + buf.append("Selected members: "); + if (members.isEmpty()) { + buf.append("NONE"); + } else { + buf.append(members.get(0).key()); + for (int i = 1; i < members.size(); ++i) { + buf.append(", ").append(members.get(i).key()); + } + } + System.out.println(buf); + } + + private static List membersFor(ElementCatalog catalog, Object... elementKeys) { + + final List eKeyList = Arrays.asList(elementKeys); + final Comparator comparator = UnionVersion::compareTo; + final Map> unionVersions = new TreeMap<>(comparator.reversed()); + for (Object elementKey : elementKeys) { + final Element e = catalog.get(elementKey); + if (e == null) { + throw new RuntimeException("Element " + elementKey + " not found in the catalog"); + } + for (Member m : e.members()) { + for (UnionVersion uv : m.unions()) { + unionVersions.computeIfAbsent(uv, v -> new HashMap<>()).put(m.key() + ":" + m.version(), m); + } + } + } + + for (Map members : unionVersions.values()) { + final Set memberElementKeys = new HashSet<>(); + final Iterator i = members.values().iterator(); + Member m = null; + while (i.hasNext()) { + m = i.next(); + memberElementKeys.addAll(m.elementKeys()); + } + if (memberElementKeys.containsAll(eKeyList)) { + return new ArrayList<>(members.values()); + } + } + return Collections.emptyList(); + } + + private static void log(ElementCatalog catalog) { + catalog.elements().stream().map(Element::key).sorted().map(catalog::get).forEach(e -> { + System.out.println("element: " + e.key()); + for (Member m : e.members()) { + System.out.println(" member: " + m.key() + " @ union: " + m.initialUnion().verion()); + } + }); + } +} diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/Union.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/Union.java new file mode 100644 index 00000000000000..2b783dc6f4a91c --- /dev/null +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/Union.java @@ -0,0 +1,29 @@ +package io.quarkus.registry.union; + +import java.util.Collection; + +public interface Union { + + /** + * Union version. + * + * @return union version + */ + UnionVersion verion(); + + /** + * Members of the union. + * + * @return members of the union + */ + Collection members(); + + /** + * Returns a union member associated with a key or null, in case + * the union does not contain a member associated with the key. + * + * @param memberKey member key + * @return member corresponding to the key or null, in case the member with the key was not found in the union + */ + Member member(Object memberKey); +} diff --git a/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/UnionVersion.java b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/UnionVersion.java new file mode 100644 index 00000000000000..aa502d1a34a559 --- /dev/null +++ b/independent-projects/tools/registry-client/src/main/java/io/quarkus/registry/union/UnionVersion.java @@ -0,0 +1,5 @@ +package io.quarkus.registry.union; + +public interface UnionVersion extends Comparable { + +}