From bb4a531f61a9961333a57c0b538184175c3b26d5 Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Tue, 16 Jun 2020 12:45:29 -0300 Subject: [PATCH 1/3] Make ValueMap.getValue generic-friendly --- .../main/java/io/quarkus/devtools/commands/data/ValueMap.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/data/ValueMap.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/data/ValueMap.java index 8d99fc794f6e1..86033d44ded91 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/data/ValueMap.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/data/ValueMap.java @@ -21,7 +21,7 @@ public ValueMap(ValueMap values) { this(values.values); } - public Object getValue(String name) { + public T getValue(String name) { return getValue(name, null); } From a08b23790d53753f4236f14ce453e3ebce824c29 Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Wed, 24 Jun 2020 11:47:06 -0300 Subject: [PATCH 2/3] Collect managed dependencies from POM hierarchy to resolve the used platform BOM --- .../quarkus/maven/QuarkusProjectMojoBase.java | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectMojoBase.java b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectMojoBase.java index c7da8d80246bf..8c5b44ce74a01 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectMojoBase.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectMojoBase.java @@ -3,11 +3,12 @@ import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import org.apache.maven.model.Dependency; import org.apache.maven.model.DependencyManagement; +import org.apache.maven.model.Model; +import org.apache.maven.model.Parent; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Component; @@ -18,9 +19,12 @@ import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.resolution.ArtifactDescriptorResult; import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; +import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver; +import io.quarkus.bootstrap.resolver.maven.workspace.ModelUtils; import io.quarkus.devtools.project.BuildTool; import io.quarkus.devtools.project.QuarkusProject; import io.quarkus.platform.descriptor.CombinedQuarkusPlatformDescriptor; @@ -75,7 +79,7 @@ private QuarkusPlatformDescriptor resolvePlatformDescriptor(final MessageWriter .setRemoteRepositories(repos).build(); if (project.getFile() != null) { final List descrArtifactList = new ArrayList<>(2); - for (Dependency dep : getManagedDependencies()) { + for (Dependency dep : getManagedDependencies(mvn)) { if ((dep.getScope() == null || !dep.getScope().equals("import")) && (dep.getType() == null || !dep.getType().equals("pom"))) { continue; @@ -164,9 +168,36 @@ private String resolveValue(String expr) throws IOException { return expr; } - private List getManagedDependencies() throws IOException { - final DependencyManagement managed = project.getModel().getDependencyManagement(); - return managed != null ? managed.getDependencies() - : Collections.emptyList(); + private List getManagedDependencies(MavenArtifactResolver resolver) throws IOException { + List managedDependencies = new ArrayList<>(); + Model model = project.getOriginalModel(); + DependencyManagement managed = model.getDependencyManagement(); + if (managed != null) { + managedDependencies.addAll(managed.getDependencies()); + } + Parent parent; + while ((parent = model.getParent()) != null) { + try { + ArtifactDescriptorResult result = resolver.resolveDescriptor(new DefaultArtifact( + parent.getGroupId(), + parent.getArtifactId(), + "pom", + ModelUtils.resolveVersion(parent.getVersion(), model))); + model = ModelUtils.readModel(result.getArtifact().getFile().toPath()); + managed = model.getDependencyManagement(); + if (managed != null) { + // Alexey Loubyansky: In Maven whatever is imported first has a priority + // So to match the maven way, we should be reading the root parent first + managedDependencies.addAll(0, managed.getDependencies()); + } + } catch (BootstrapMavenException e) { + // ignore + if (getLog().isDebugEnabled()) { + getLog().debug("Error while resolving descriptor", e); + } + break; + } + } + return managedDependencies; } } From f7bafc611094ed64d144f3a5d0e0e7dd71002361 Mon Sep 17 00:00:00 2001 From: George Gastaldi Date: Tue, 16 Jun 2020 12:46:42 -0300 Subject: [PATCH 3/3] Support ExtensionRegistry in ListExtensions Support ExtensionRegistry in AddExtensions Support multiple registries Introduced registry-descriptor and API Introduced ExtensionPredicate Add Extensions now use ExtensionRegistry.lookup Fixes #9593 --- .../gradle/tasks/QuarkusAddExtension.java | 28 ++- .../gradle/tasks/QuarkusListExtensions.java | 31 ++- .../gradle/tasks/QuarkusPlatformTask.java | 11 + .../io/quarkus/maven/AddExtensionMojo.java | 23 +- .../io/quarkus/maven/ListExtensionsMojo.java | 19 +- .../quarkus/maven/QuarkusProjectMojoBase.java | 4 +- .../tools/devtools-common/pom.xml | 6 + .../devtools/commands/AddExtensions.java | 7 + .../devtools/commands/ListExtensions.java | 7 + .../handlers/AddExtensionsCommandHandler.java | 18 +- .../ListExtensionsCommandHandler.java | 48 ++-- .../handlers/QuarkusCommandHandlers.java | 7 +- .../buildfile/AbstractGradleBuildFile.java | 97 +++++---- .../devtools/project/buildfile/BuildFile.java | 40 +++- .../buildfile/GenericGradleBuildFile.java | 6 + .../buildfile/GradleBuildFilesCreator.java | 17 +- .../project/buildfile/MavenBuildFile.java | 87 ++++++-- .../extensions/ExtensionInstallPlan.java | 108 +++++++++ .../project/extensions/ExtensionManager.java | 15 ++ .../registry/DefaultExtensionRegistry.java | 206 ++++++++++++++++++ .../quarkus/registry/ExtensionRegistry.java | 33 +++ .../quarkus/registry/RepositoryIndexer.java | 45 ++++ .../builder/ExtensionRegistryBuilder.java | 48 ++++ .../registry/builder/RegistryBuilder.java | 123 +++++++++++ .../registry/builder/URLRegistryBuilder.java | 51 +++++ .../registry/catalog/model/Extension.java | 28 +++ .../registry/catalog/model/Platform.java | 42 ++++ .../registry/catalog/model/Repository.java | 60 +++++ .../catalog/spi/ArtifactResolver.java | 22 ++ .../registry/catalog/spi/IndexVisitor.java | 11 + .../registry/model/ArtifactCoords.java | 16 ++ .../quarkus/registry/model/ArtifactKey.java | 17 ++ .../io/quarkus/registry/model/Extension.java | 58 +++++ .../io/quarkus/registry/model/Nullable.java | 4 + .../io/quarkus/registry/model/Platform.java | 18 ++ .../io/quarkus/registry/model/Registry.java | 29 +++ .../io/quarkus/registry/model/Release.java | 37 ++++ .../io/quarkus/registry/package-info.java | 6 + .../commands/AbstractAddExtensionsTest.java | 5 +- .../devtools/commands/ListExtensionsTest.java | 2 +- .../project/buildfile/MavenBuildFileTest.java | 52 +++++ .../registry/RepositoryIndexerTest.java | 32 +++ .../registry/TestArtifactResolver.java | 74 +++++++ .../builder/ExtensionRegistryBuilderTest.java | 38 ++++ .../registry/builder/RegistryBuilderTest.java | 59 +++++ .../builder/URLRegistryBuilderTest.java | 24 ++ .../registry/model/RepositoryTest.java | 19 ++ .../src/test/resources/registry/registry.json | 202 +++++++++++++++++ .../registry/repository/extensions/jsf.json | 14 ++ .../registry/repository/platforms.json | 30 +++ .../tools/platform-descriptor-api/pom.xml | 5 + .../dependencies/ExtensionPredicate.java | 143 ++++++++++++ .../dependencies/ExtensionPredicateTest.java | 32 +++ independent-projects/tools/pom.xml | 11 + .../registry-descriptor-resolver/pom.xml | 37 ++++ .../resolver/DefaultArtifactResolver.java | 72 ++++++ .../resolver/DefaultArtifactResolverTest.java | 51 +++++ .../DefaultExtensionRegistryTest.java | 95 ++++++++ .../resources/repository/extensions/jsf.json | 14 ++ .../test/resources/repository/platforms.json | 30 +++ 60 files changed, 2335 insertions(+), 139 deletions(-) create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/extensions/ExtensionInstallPlan.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/DefaultExtensionRegistry.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/ExtensionRegistry.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/RepositoryIndexer.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/builder/ExtensionRegistryBuilder.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/builder/RegistryBuilder.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/builder/URLRegistryBuilder.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/model/Extension.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/model/Platform.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/model/Repository.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/spi/ArtifactResolver.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/spi/IndexVisitor.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/ArtifactCoords.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/ArtifactKey.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Extension.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Nullable.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Platform.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Registry.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Release.java create mode 100644 independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/package-info.java create mode 100644 independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/buildfile/MavenBuildFileTest.java create mode 100644 independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/RepositoryIndexerTest.java create mode 100644 independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/TestArtifactResolver.java create mode 100644 independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/builder/ExtensionRegistryBuilderTest.java create mode 100644 independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/builder/RegistryBuilderTest.java create mode 100644 independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/builder/URLRegistryBuilderTest.java create mode 100644 independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/model/RepositoryTest.java create mode 100644 independent-projects/tools/devtools-common/src/test/resources/registry/registry.json create mode 100644 independent-projects/tools/devtools-common/src/test/resources/registry/repository/extensions/jsf.json create mode 100644 independent-projects/tools/devtools-common/src/test/resources/registry/repository/platforms.json create mode 100644 independent-projects/tools/platform-descriptor-api/src/main/java/io/quarkus/dependencies/ExtensionPredicate.java create mode 100644 independent-projects/tools/platform-descriptor-api/src/test/java/io/quarkus/dependencies/ExtensionPredicateTest.java create mode 100644 independent-projects/tools/registry-descriptor-resolver/pom.xml create mode 100644 independent-projects/tools/registry-descriptor-resolver/src/main/java/io/quarkus/registry/resolver/DefaultArtifactResolver.java create mode 100644 independent-projects/tools/registry-descriptor-resolver/src/test/java/io/quarkus/registry/resolver/DefaultArtifactResolverTest.java create mode 100644 independent-projects/tools/registry-descriptor-resolver/src/test/java/io/quarkus/registry/resolver/DefaultExtensionRegistryTest.java create mode 100644 independent-projects/tools/registry-descriptor-resolver/src/test/resources/repository/extensions/jsf.json create mode 100644 independent-projects/tools/registry-descriptor-resolver/src/test/resources/repository/platforms.json diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusAddExtension.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusAddExtension.java index 7037b775355da..cab182f13f01e 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusAddExtension.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusAddExtension.java @@ -1,17 +1,21 @@ package io.quarkus.gradle.tasks; import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; +import java.net.URL; import java.util.List; import java.util.Set; import org.gradle.api.GradleException; import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.options.Option; import io.quarkus.devtools.commands.AddExtensions; +import io.quarkus.registry.DefaultExtensionRegistry; public class QuarkusAddExtension extends QuarkusPlatformTask { @@ -20,6 +24,7 @@ public QuarkusAddExtension() { } private List extensionsToAdd; + private List registries; @Option(option = "extensions", description = "Configures the extensions to be added.") public void setExtensionsToAdd(List extensionsToAdd) { @@ -31,6 +36,17 @@ public List getExtensionsToAdd() { return extensionsToAdd; } + @Optional + @Input + public List getRegistries() { + return registries; + } + + @Option(description = "The extension registry URLs to be used", option = "registry") + public void setRegistries(List registry) { + this.registries = registry; + } + @TaskAction public void addExtension() { Set extensionsSet = getExtensionsToAdd() @@ -40,9 +56,15 @@ public void addExtension() { .collect(toSet()); try { - new AddExtensions(getQuarkusProject()) - .extensions(extensionsSet) - .execute(); + AddExtensions addExtensions = new AddExtensions(getQuarkusProject()) + .extensions(extensionsSet); + if (registries != null && !registries.isEmpty()) { + List urls = registries.stream() + .map(QuarkusAddExtension::toURL) + .collect(toList()); + addExtensions.extensionRegistry(DefaultExtensionRegistry.fromURLs(urls)); + } + addExtensions.execute(); } catch (Exception e) { throw new GradleException("Failed to add extensions " + getExtensionsToAdd(), e); } diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusListExtensions.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusListExtensions.java index aad55911e3c29..2c9407c9dde80 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusListExtensions.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusListExtensions.java @@ -1,5 +1,10 @@ package io.quarkus.gradle.tasks; +import static java.util.stream.Collectors.toList; + +import java.net.URL; +import java.util.List; + import org.gradle.api.GradleException; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Optional; @@ -7,6 +12,7 @@ import org.gradle.api.tasks.options.Option; import io.quarkus.devtools.commands.ListExtensions; +import io.quarkus.registry.DefaultExtensionRegistry; public class QuarkusListExtensions extends QuarkusPlatformTask { @@ -16,6 +22,8 @@ public class QuarkusListExtensions extends QuarkusPlatformTask { private String searchPattern; + private List registries; + @Input public boolean isAll() { return all; @@ -48,6 +56,17 @@ public void setSearchPattern(String searchPattern) { this.searchPattern = searchPattern; } + @Optional + @Input + public List getRegistries() { + return registries; + } + + @Option(description = "The extension registry URLs to be used", option = "registry") + public void setRegistries(List registry) { + this.registries = registry; + } + public QuarkusListExtensions() { super("Lists the available quarkus extensions"); } @@ -55,11 +74,17 @@ public QuarkusListExtensions() { @TaskAction public void listExtensions() { try { - new ListExtensions(getQuarkusProject()) + ListExtensions listExtensions = new ListExtensions(getQuarkusProject()) .all(isAll()) .format(getFormat()) - .search(getSearchPattern()) - .execute(); + .search(getSearchPattern()); + if (registries != null && !registries.isEmpty()) { + List urls = registries.stream() + .map(QuarkusListExtensions::toURL) + .collect(toList()); + listExtensions.extensionRegistry(DefaultExtensionRegistry.fromURLs(urls)); + } + listExtensions.execute(); } catch (Exception e) { throw new GradleException("Unable to list extensions", e); } diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusPlatformTask.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusPlatformTask.java index 6c2c2948ea15c..f85ac2ebe0efd 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusPlatformTask.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusPlatformTask.java @@ -1,5 +1,7 @@ package io.quarkus.gradle.tasks; +import java.net.MalformedURLException; +import java.net.URL; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -78,4 +80,13 @@ protected BuildFile getGradleBuildFile() { protected QuarkusProject getQuarkusProject() { return QuarkusProject.of(getProject().getProjectDir().toPath(), platformDescriptor(), getGradleBuildFile()); } + + protected static URL toURL(String url) { + try { + return new URL(url); + } catch (MalformedURLException e) { + throw new GradleException("Malformed URL:" + url, e); + } + } + } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/AddExtensionMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/AddExtensionMojo.java index 4442aa26a4cfb..49cf36a28e9f6 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/AddExtensionMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/AddExtensionMojo.java @@ -1,9 +1,12 @@ package io.quarkus.maven; +import static java.util.stream.Collectors.toSet; + +import java.net.URL; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; -import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.maven.plugin.MojoExecutionException; @@ -14,6 +17,7 @@ import io.quarkus.devtools.commands.data.QuarkusCommandOutcome; import io.quarkus.devtools.project.QuarkusProject; import io.quarkus.platform.tools.MessageWriter; +import io.quarkus.registry.DefaultExtensionRegistry; /** * Allow adding an extension to an existing pom.xml file. @@ -36,6 +40,12 @@ public class AddExtensionMojo extends QuarkusProjectMojoBase { @Parameter(property = "extension") String extension; + /** + * The URL where the registry is. + */ + @Parameter(property = "registry", alias = "quarkus.extension.registry") + List registries; + @Override protected void validateParameters() throws MojoExecutionException { if ((StringUtils.isBlank(extension) && (extensions == null || extensions.isEmpty())) // None are set @@ -53,13 +63,16 @@ public void doExecute(final QuarkusProject quarkusProject, final MessageWriter l } else { // Parse the "extension" just in case it contains several comma-separated values // https://github.com/quarkusio/quarkus/issues/2393 - ext.addAll(Arrays.stream(extension.split(",")).map(s -> s.trim()).collect(Collectors.toSet())); + ext.addAll(Arrays.stream(extension.split(",")).map(String::trim).collect(toSet())); } try { - final QuarkusCommandOutcome outcome = new AddExtensions(quarkusProject) - .extensions(ext.stream().map(String::trim).collect(Collectors.toSet())) - .execute(); + AddExtensions addExtensions = new AddExtensions(quarkusProject) + .extensions(ext.stream().map(String::trim).collect(toSet())); + if (registries != null && !registries.isEmpty()) { + addExtensions.extensionRegistry(DefaultExtensionRegistry.fromURLs(registries)); + } + final QuarkusCommandOutcome outcome = addExtensions.execute(); if (!outcome.isSuccess()) { throw new MojoExecutionException("Unable to add extensions"); } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/ListExtensionsMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/ListExtensionsMojo.java index 91ae34967b52d..d129f98302cc8 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/ListExtensionsMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/ListExtensionsMojo.java @@ -1,5 +1,8 @@ package io.quarkus.maven; +import java.net.URL; +import java.util.List; + import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; @@ -7,6 +10,7 @@ import io.quarkus.devtools.commands.ListExtensions; import io.quarkus.devtools.project.QuarkusProject; import io.quarkus.platform.tools.MessageWriter; +import io.quarkus.registry.DefaultExtensionRegistry; /** * List the available extensions. @@ -36,14 +40,23 @@ public class ListExtensionsMojo extends QuarkusProjectMojoBase { @Parameter(property = "searchPattern", alias = "quarkus.extension.searchPattern") protected String searchPattern; + /** + * The extension registry URLs + */ + @Parameter(property = "registry", alias = "quarkus.extension.registry") + List registries; + @Override public void doExecute(final QuarkusProject quarkusProject, final MessageWriter log) throws MojoExecutionException { try { - new ListExtensions(quarkusProject) + ListExtensions listExtensions = new ListExtensions(quarkusProject) .all(all) .format(format) - .search(searchPattern) - .execute(); + .search(searchPattern); + if (registries != null && !registries.isEmpty()) { + listExtensions.extensionRegistry(DefaultExtensionRegistry.fromURLs(registries)); + } + listExtensions.execute(); } catch (Exception e) { throw new MojoExecutionException("Failed to list extensions", e); } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectMojoBase.java b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectMojoBase.java index 8c5b44ce74a01..2b3d192471a93 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectMojoBase.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusProjectMojoBase.java @@ -19,7 +19,7 @@ import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.repository.RemoteRepository; -import org.eclipse.aether.resolution.ArtifactDescriptorResult; +import org.eclipse.aether.resolution.ArtifactResult; import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; @@ -178,7 +178,7 @@ private List getManagedDependencies(MavenArtifactResolver resolver) Parent parent; while ((parent = model.getParent()) != null) { try { - ArtifactDescriptorResult result = resolver.resolveDescriptor(new DefaultArtifact( + ArtifactResult result = resolver.resolve(new DefaultArtifact( parent.getGroupId(), parent.getArtifactId(), "pom", diff --git a/independent-projects/tools/devtools-common/pom.xml b/independent-projects/tools/devtools-common/pom.xml index 1c173d9c28919..45b21dcf05540 100644 --- a/independent-projects/tools/devtools-common/pom.xml +++ b/independent-projects/tools/devtools-common/pom.xml @@ -106,6 +106,12 @@ jakarta.websocket jakarta.websocket-api + + org.immutables + value + 2.8.8 + provided + org.junit.jupiter junit-jupiter diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/AddExtensions.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/AddExtensions.java index 65bd6a79da2ab..f6b614c32376f 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/AddExtensions.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/AddExtensions.java @@ -10,6 +10,7 @@ import io.quarkus.devtools.project.extensions.ExtensionManager; import io.quarkus.platform.tools.ToolsConstants; import io.quarkus.platform.tools.ToolsUtils; +import io.quarkus.registry.ExtensionRegistry; import java.util.Set; /** @@ -21,6 +22,7 @@ public class AddExtensions { public static final String EXTENSIONS = ToolsUtils.dotJoin(ToolsConstants.QUARKUS, NAME, "extensions"); public static final String OUTCOME_UPDATED = ToolsUtils.dotJoin(ToolsConstants.QUARKUS, NAME, "outcome", "updated"); public static final String EXTENSION_MANAGER = ToolsUtils.dotJoin(ToolsConstants.QUARKUS, NAME, "extension-manager"); + public static final String EXTENSION_REGISTRY = ToolsUtils.dotJoin(ToolsConstants.QUARKUS, NAME, "extension-registry"); private final QuarkusCommandInvocation invocation; private final AddExtensionsCommandHandler handler = new AddExtensionsCommandHandler(); @@ -34,6 +36,11 @@ public AddExtensions extensionManager(ExtensionManager extensionManager) { return this; } + public AddExtensions extensionRegistry(ExtensionRegistry extensionRegistry) { + invocation.setValue(EXTENSION_REGISTRY, requireNonNull(extensionRegistry, "extensionRegistry is required")); + return this; + } + public AddExtensions extensions(Set extensions) { invocation.setValue(EXTENSIONS, extensions); return this; diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/ListExtensions.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/ListExtensions.java index 235c617b6331b..614be17d3f842 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/ListExtensions.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/ListExtensions.java @@ -10,6 +10,7 @@ import io.quarkus.devtools.project.extensions.ExtensionManager; import io.quarkus.platform.tools.ToolsConstants; import io.quarkus.platform.tools.ToolsUtils; +import io.quarkus.registry.ExtensionRegistry; /** * Instances of this class are not thread-safe. They are created per single invocation. @@ -21,6 +22,7 @@ public class ListExtensions { public static final String FORMAT = ToolsUtils.dotJoin(PARAM_PREFIX, "format"); public static final String SEARCH = ToolsUtils.dotJoin(PARAM_PREFIX, "search"); public static final String EXTENSION_MANAGER = ToolsUtils.dotJoin(PARAM_PREFIX, "extension-manager"); + public static final String EXTENSION_REGISTRY = ToolsUtils.dotJoin(PARAM_PREFIX, "extension-registry"); private final QuarkusCommandInvocation invocation; private final ListExtensionsCommandHandler handler = new ListExtensionsCommandHandler(); @@ -44,6 +46,11 @@ public ListExtensions extensionManager(ExtensionManager extensionManager) { return this; } + public ListExtensions extensionRegistry(ExtensionRegistry extensionRegistry) { + invocation.setValue(EXTENSION_REGISTRY, requireNonNull(extensionRegistry, "extensionRegistry is required")); + return this; + } + public ListExtensions search(String search) { invocation.setValue(SEARCH, search); return this; diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/AddExtensionsCommandHandler.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/AddExtensionsCommandHandler.java index 2aeb4455eb847..95126446c1975 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/AddExtensionsCommandHandler.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/AddExtensionsCommandHandler.java @@ -1,19 +1,19 @@ package io.quarkus.devtools.commands.handlers; import static io.quarkus.devtools.commands.AddExtensions.EXTENSION_MANAGER; -import static io.quarkus.devtools.commands.handlers.QuarkusCommandHandlers.computeCoordsFromQuery; -import io.quarkus.bootstrap.model.AppArtifactCoords; import io.quarkus.devtools.commands.AddExtensions; import io.quarkus.devtools.commands.data.QuarkusCommandException; import io.quarkus.devtools.commands.data.QuarkusCommandInvocation; import io.quarkus.devtools.commands.data.QuarkusCommandOutcome; +import io.quarkus.devtools.project.extensions.ExtensionInstallPlan; import io.quarkus.devtools.project.extensions.ExtensionManager; import io.quarkus.devtools.project.extensions.ExtensionManager.InstallResult; import io.quarkus.platform.tools.ConsoleMessageFormats; +import io.quarkus.registry.DefaultExtensionRegistry; +import io.quarkus.registry.ExtensionRegistry; import java.io.IOException; import java.util.Collections; -import java.util.List; import java.util.Set; /** @@ -29,12 +29,18 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws return QuarkusCommandOutcome.success().setValue(AddExtensions.OUTCOME_UPDATED, false); } - final List extensionsToAdd = computeCoordsFromQuery(invocation, extensionsQuery); + ExtensionRegistry extensionRegistry = invocation.getValue(AddExtensions.EXTENSION_REGISTRY); + if (extensionRegistry == null) { + extensionRegistry = DefaultExtensionRegistry.fromPlatform(invocation.getPlatformDescriptor()); + } + String quarkusVersion = invocation.getPlatformDescriptor().getQuarkusVersion(); + ExtensionInstallPlan extensionInstallPlan = extensionRegistry.planInstallation(quarkusVersion, extensionsQuery); + final ExtensionManager extensionManager = invocation.getValue(EXTENSION_MANAGER, invocation.getQuarkusProject().getExtensionManager()); try { - if (extensionsToAdd != null) { - final InstallResult result = extensionManager.install(extensionsToAdd); + if (extensionInstallPlan.isNotEmpty()) { + final InstallResult result = extensionManager.install(extensionInstallPlan); result.getInstalled() .forEach(a -> invocation.log() .info(ConsoleMessageFormats.ok("Extension " + a.getGroupId() + ":" + a.getArtifactId()) diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/ListExtensionsCommandHandler.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/ListExtensionsCommandHandler.java index e5fc7aa261285..76ff92b02ee4b 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/ListExtensionsCommandHandler.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/ListExtensionsCommandHandler.java @@ -13,15 +13,14 @@ import io.quarkus.devtools.project.BuildTool; import io.quarkus.devtools.project.extensions.ExtensionManager; import io.quarkus.platform.tools.MessageWriter; +import io.quarkus.registry.DefaultExtensionRegistry; +import io.quarkus.registry.ExtensionRegistry; import java.io.IOException; -import java.util.List; +import java.util.Collection; import java.util.Map; import java.util.Objects; import java.util.function.BiConsumer; import java.util.function.Function; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; /** * Instances of this class are thread-safe. It lists extensions according to the options passed in as properties of @@ -36,12 +35,16 @@ public class ListExtensionsCommandHandler implements QuarkusCommandHandler { @Override public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws QuarkusCommandException { + final MessageWriter log = invocation.log(); final boolean all = invocation.getValue(ListExtensions.ALL, true); final String format = invocation.getValue(ListExtensions.FORMAT, "concise"); final String search = invocation.getValue(ListExtensions.SEARCH, "*"); final ExtensionManager extensionManager = invocation.getValue(ListExtensions.EXTENSION_MANAGER, invocation.getQuarkusProject().getExtensionManager()); - + ExtensionRegistry extensionRegistry = invocation.getValue(ListExtensions.EXTENSION_REGISTRY); + if (extensionRegistry == null) { + extensionRegistry = DefaultExtensionRegistry.fromPlatform(invocation.getPlatformDescriptor()); + } Map installedByKey; try { installedByKey = extensionManager.getInstalled().stream() @@ -49,20 +52,13 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws } catch (IOException e) { throw new QuarkusCommandException("Failed to determine the list of installed extensions", e); } - - Stream platformExtensionsStream = invocation.getPlatformDescriptor().getExtensions().stream(); - platformExtensionsStream = platformExtensionsStream.filter(this::filterUnlisted); - if (search != null && !"*".equalsIgnoreCase(search)) { - final Pattern searchPattern = Pattern.compile(".*" + search + ".*", Pattern.CASE_INSENSITIVE); - platformExtensionsStream = platformExtensionsStream.filter(e -> filterBySearch(searchPattern, e)); - } - List platformExtensions = platformExtensionsStream.collect(Collectors.toList()); - + String quarkusVersion = invocation.getPlatformDescriptor().getQuarkusVersion(); + Collection platformExtensions = extensionRegistry.list(quarkusVersion, search); if (platformExtensions.isEmpty()) { - invocation.log().info("No extension found with this pattern"); + log.info("No extension found with pattern '%s'", search); } else { String extensionStatus = all ? "available" : "installable"; - invocation.log().info(String.format("%nCurrent Quarkus extensions %s: ", extensionStatus)); + log.info("%nCurrent Quarkus extensions %s: ", extensionStatus); BiConsumer currentFormatter; switch (format.toLowerCase()) { @@ -71,7 +67,7 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws break; case "full": currentFormatter = this::fullFormatter; - currentFormatter.accept(invocation.log(), + currentFormatter.accept(log, new String[] { "Status", "Extension", "ArtifactId", "Updated Version", "Guide" }); break; case "concise": @@ -79,23 +75,23 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws currentFormatter = this::conciseFormatter; } - platformExtensions.forEach(platformExtension -> display(invocation.log(), platformExtension, + platformExtensions.forEach(platformExtension -> display(log, platformExtension, installedByKey.get(toKey(platformExtension)), all, currentFormatter)); final BuildTool buildTool = invocation.getQuarkusProject().getBuildTool(); if ("concise".equalsIgnoreCase(format)) { if (BuildTool.GRADLE.equals(buildTool)) { - invocation.log().info("\nTo get more information, append --format=full to your command line."); + log.info("\nTo get more information, append --format=full to your command line."); } else { - invocation.log().info( + log.info( "\nTo get more information, append -Dquarkus.extension.format=full to your command line."); } } if (BuildTool.GRADLE.equals(buildTool)) { - invocation.log().info("\nAdd an extension to your project by adding the dependency to your " + + log.info("\nAdd an extension to your project by adding the dependency to your " + "build.gradle or use `./gradlew addExtension --extensions=\"artifactId\"`"); } else { - invocation.log().info("\nAdd an extension to your project by adding the dependency to your " + + log.info("\nAdd an extension to your project by adding the dependency to your " + "pom.xml or use `./mvnw quarkus:add-extension -Dextensions=\"artifactId\"`"); } @@ -104,14 +100,6 @@ public QuarkusCommandOutcome execute(QuarkusCommandInvocation invocation) throws return QuarkusCommandOutcome.success(); } - private boolean filterUnlisted(Extension e) { - return !e.getMetadata().containsKey("unlisted"); - } - - private boolean filterBySearch(final Pattern searchPattern, Extension e) { - return searchPattern.matcher(e.getName()).matches(); - } - private void conciseFormatter(MessageWriter writer, String[] cols) { writer.info(String.format(CONCISE_FORMAT, cols[1], cols[2], cols[4])); } diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/QuarkusCommandHandlers.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/QuarkusCommandHandlers.java index 18c976d293f76..b0dfd2d898dc2 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/QuarkusCommandHandlers.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/commands/handlers/QuarkusCommandHandlers.java @@ -10,6 +10,7 @@ import io.quarkus.devtools.commands.data.SelectionResult; import io.quarkus.devtools.project.extensions.Extensions; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; @@ -34,7 +35,8 @@ static List computeCoordsFromQuery(final QuarkusCommandInvoca } else if (countColons > 1) { builder.add(AppArtifactCoords.fromString(query)); } else { - SelectionResult result = select(query, invocation.getPlatformDescriptor().getExtensions(), false); + Collection extensions = invocation.getPlatformDescriptor().getExtensions(); + SelectionResult result = select(query, extensions, false); if (result.matches()) { final Set withStrippedVersion = result.getExtensions().stream().map(Extensions::toCoords) .map(Extensions::stripVersion).collect(Collectors.toSet()); @@ -74,7 +76,8 @@ static List computeCoordsFromQuery(final QuarkusCommandInvoca * be {@code false} by default. * @return the list of matching candidates and whether or not a match has been found. */ - static SelectionResult select(final String query, final List allPlatformExtensions, final boolean labelLookup) { + static SelectionResult select(final String query, final Collection allPlatformExtensions, + final boolean labelLookup) { String q = query.trim().toLowerCase(); // Try exact matches diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/AbstractGradleBuildFile.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/AbstractGradleBuildFile.java index 6c85b42629e85..ab60cfbde3a05 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/AbstractGradleBuildFile.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/AbstractGradleBuildFile.java @@ -11,11 +11,9 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Optional; import java.util.Properties; import java.util.Scanner; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; // We keep it here to take advantage of the abstract tests public abstract class AbstractGradleBuildFile extends BuildFile { @@ -24,28 +22,27 @@ public abstract class AbstractGradleBuildFile extends BuildFile { private static final String SETTINGS_GRADLE_PATH = "settings.gradle"; private static final String GRADLE_PROPERTIES_PATH = "gradle.properties"; - private final Optional rootProjectPath; + private final Path rootProjectPath; private AtomicReference modelReference = new AtomicReference<>(); public AbstractGradleBuildFile(final Path projectDirPath, final QuarkusPlatformDescriptor platformDescriptor) { - super(projectDirPath, platformDescriptor); - this.rootProjectPath = Optional.empty(); + this(projectDirPath, platformDescriptor, null); } public AbstractGradleBuildFile(final Path projectDirPath, final QuarkusPlatformDescriptor platformDescriptor, Path rootProjectPath) { super(projectDirPath, platformDescriptor); - this.rootProjectPath = Optional.ofNullable(rootProjectPath); + this.rootProjectPath = rootProjectPath; } @Override public void writeToDisk() throws IOException { - if (rootProjectPath.isPresent()) { - Files.write(rootProjectPath.get().resolve(SETTINGS_GRADLE_PATH), getModel().getRootSettingsContent().getBytes()); + if (rootProjectPath != null) { + Files.write(rootProjectPath.resolve(SETTINGS_GRADLE_PATH), getModel().getRootSettingsContent().getBytes()); try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { getModel().getRootPropertiesContent().store(out, "Gradle properties"); - Files.write(rootProjectPath.get().resolve(GRADLE_PROPERTIES_PATH), + Files.write(rootProjectPath.resolve(GRADLE_PROPERTIES_PATH), out.toByteArray()); } } else { @@ -59,32 +56,45 @@ public void writeToDisk() throws IOException { } @Override - protected void addDependencyInBuildFile(AppArtifactCoords coords) throws IOException { - addDependencyInModel(getModel(), coords); - } - - static void addDependencyInModel(Model model, AppArtifactCoords coords) throws IOException { - StringBuilder newBuildContent = new StringBuilder(); - readLineByLine(model.getBuildContent(), currentLine -> { - newBuildContent.append(currentLine).append(System.lineSeparator()); - if (currentLine.startsWith("dependencies {")) { - newBuildContent.append(" implementation '") - .append(coords.getGroupId()) - .append(":") - .append(coords.getArtifactId()); - if (coords.getVersion() != null && !coords.getVersion().isEmpty()) { - newBuildContent.append(":") - .append(coords.getVersion()); - } - newBuildContent.append("'") - .append(System.lineSeparator()); + protected boolean addDependency(AppArtifactCoords coords, boolean managed) { + return addDependencyInModel(getModel(), coords, managed); + } + + static boolean addDependencyInModel(Model model, AppArtifactCoords coords, boolean managed) { + StringBuilder newDependency = new StringBuilder() + .append(" implementation '") + .append(coords.getGroupId()) + .append(":") + .append(coords.getArtifactId()); + if (!managed && + (coords.getVersion() != null && !coords.getVersion().isEmpty())) { + newDependency.append(":").append(coords.getVersion()); + } + newDependency.append("'").append(System.lineSeparator()); + String newDependencyString = newDependency.toString(); + StringBuilder buildContent = new StringBuilder(model.getBuildContent()); + boolean changed = false; + if (buildContent.indexOf(newDependencyString) == -1) { + changed = true; + // Add dependency after "dependencies {" + int indexOfDeps = buildContent.indexOf("dependencies {"); + if (indexOfDeps > -1) { + // The line below fails on Windows if System.lineSeparator() is used + int nextLine = buildContent.indexOf("\n", indexOfDeps) + 1; + buildContent.insert(nextLine, newDependencyString); + } else { + // if no "dependencies {" found, add one + buildContent.append("dependencies {").append(System.lineSeparator()) + .append(newDependencyString) + .append("}").append(System.lineSeparator()); } - }); - model.setBuildContent(newBuildContent.toString()); + model.setBuildContent(buildContent.toString()); + } + return changed; } @Override - protected void removeDependencyFromBuildFile(AppArtifactKey key) throws IOException { + protected void removeDependency(AppArtifactKey key) { StringBuilder newBuildContent = new StringBuilder(); Scanner scanner = new Scanner(getModel().getBuildContent()); while (scanner.hasNextLine()) { @@ -98,7 +108,7 @@ protected void removeDependencyFromBuildFile(AppArtifactKey key) throws IOExcept } @Override - public String getProperty(String propertyName) throws IOException { + public String getProperty(String propertyName) { final String property = getModel().getPropertiesContent().getProperty(propertyName); if (property != null || getModel().getRootPropertiesContent() == null) { return property; @@ -111,7 +121,7 @@ public BuildTool getBuildTool() { return BuildTool.GRADLE; } - private Model getModel() throws IOException { + private Model getModel() { return modelReference.updateAndGet(model -> { if (model == null) { try { @@ -130,17 +140,18 @@ protected void refreshData() { } private boolean hasRootProjectFile(final String fileName) throws IOException { - if (!rootProjectPath.isPresent()) { + if (rootProjectPath == null) { return false; } - final Path filePath = rootProjectPath.get().resolve(fileName); + final Path filePath = rootProjectPath.resolve(fileName); return Files.exists(filePath); } private byte[] readRootProjectFile(final String fileName) throws IOException { - final Path filePath = rootProjectPath - .orElseThrow(() -> new IllegalStateException("There is no rootProject defined in this GradleBuildFile")) - .resolve(fileName); + if (rootProjectPath == null) { + throw new IllegalStateException("There is no rootProject defined in this GradleBuildFile"); + } + final Path filePath = rootProjectPath.resolve(fileName); return Files.readAllBytes(filePath); } @@ -178,16 +189,6 @@ protected String getBuildContent() throws IOException { return getModel().getBuildContent(); } - private static void readLineByLine(String content, Consumer lineConsumer) { - try (Scanner scanner = new Scanner(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)), - StandardCharsets.UTF_8.name())) { - while (scanner.hasNextLine()) { - String currentLine = scanner.nextLine(); - lineConsumer.accept(currentLine); - } - } - } - static class Model { private String settingsContent; private String buildContent; 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 97996678cad8e..61123333d1c73 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 @@ -9,6 +9,7 @@ import io.quarkus.bootstrap.model.AppArtifactCoords; import io.quarkus.bootstrap.model.AppArtifactKey; import io.quarkus.dependencies.Extension; +import io.quarkus.devtools.project.extensions.ExtensionInstallPlan; import io.quarkus.devtools.project.extensions.ExtensionManager; import io.quarkus.devtools.project.extensions.Extensions; import io.quarkus.platform.descriptor.QuarkusPlatformDescriptor; @@ -16,6 +17,7 @@ import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; @@ -39,18 +41,34 @@ public final InstallResult install(Collection coords) throws final List installed = coords.stream() .distinct() .filter(a -> !existingKeys.contains(a.getKey())) - .filter(e -> { - try { - addDependencyInBuildFile(e); - return true; - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } - }).collect(toList()); + .collect(toList()); + installed.forEach(e -> addDependency(e, e.getVersion() == null)); this.writeToDisk(); return new InstallResult(installed); } + @Override + public InstallResult install(ExtensionInstallPlan plan) throws IOException { + List installed = new ArrayList<>(); + for (AppArtifactCoords platform : plan.getPlatforms()) { + if (addDependency(platform, false)) { + installed.add(platform); + } + } + for (AppArtifactCoords managedExtension : plan.getManagedExtensions()) { + if (addDependency(managedExtension, true)) { + installed.add(managedExtension); + } + } + for (AppArtifactCoords independentExtension : plan.getIndependentExtensions()) { + if (addDependency(independentExtension, false)) { + installed.add(independentExtension); + } + } + writeToDisk(); + return new InstallResult(installed); + } + @Override public final Collection getInstalled() throws IOException { this.refreshData(); @@ -69,7 +87,7 @@ public final UninstallResult uninstall(Collection keys) throws I .filter(existingKeys::contains) .filter(k -> { try { - removeDependencyFromBuildFile(k); + removeDependency(k); return true; } catch (IOException ex) { throw new UncheckedIOException(ex); @@ -79,9 +97,9 @@ public final UninstallResult uninstall(Collection keys) throws I return new UninstallResult(uninstalled); } - protected abstract void addDependencyInBuildFile(AppArtifactCoords coords) throws IOException; + protected abstract boolean addDependency(AppArtifactCoords coords, boolean managed); - protected abstract void removeDependencyFromBuildFile(AppArtifactKey key) throws IOException; + protected abstract void removeDependency(AppArtifactKey key) throws IOException; protected abstract List getDependencies() throws IOException; diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/GenericGradleBuildFile.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/GenericGradleBuildFile.java index 29b7bf0d541e1..9a07d5ef77d1d 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/GenericGradleBuildFile.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/GenericGradleBuildFile.java @@ -3,6 +3,7 @@ import io.quarkus.bootstrap.model.AppArtifactCoords; import io.quarkus.bootstrap.model.AppArtifactKey; import io.quarkus.devtools.project.BuildTool; +import io.quarkus.devtools.project.extensions.ExtensionInstallPlan; import io.quarkus.devtools.project.extensions.ExtensionManager; import java.io.IOException; import java.util.Collection; @@ -27,6 +28,11 @@ public InstallResult install(Collection coords) throws IOExce throw new IllegalStateException("This feature is not yet implemented outside of the Gradle Plugin."); } + @Override + public InstallResult install(ExtensionInstallPlan request) throws IOException { + throw new IllegalStateException("This feature is not yet implemented outside of the Gradle Plugin."); + } + @Override public UninstallResult uninstall(Collection keys) throws IOException { throw new IllegalStateException("This feature is not yet implemented outside of the Gradle Plugin."); diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/GradleBuildFilesCreator.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/GradleBuildFilesCreator.java index 22b4e2380282b..785d71dc7d952 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/GradleBuildFilesCreator.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/buildfile/GradleBuildFilesCreator.java @@ -38,14 +38,13 @@ public void create(String groupId, String artifactId, String version, createSettingsContent(artifactId); createBuildContent(groupId, version); createProperties(); - extensions.stream() - .forEach(e -> { - try { - addDependencyInBuildFile(e); - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } - }); + extensions.forEach(e -> { + try { + addDependencyInBuildFile(e); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); this.writeToDisk(); } @@ -59,7 +58,7 @@ private void writeToDisk() throws IOException { } private void addDependencyInBuildFile(AppArtifactCoords coords) throws IOException { - addDependencyInModel(getModel(), coords); + addDependencyInModel(getModel(), coords, false); } protected boolean containsBOM(String groupId, String artifactId) 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 8ccb4cf2a963b..ec76cf0423beb 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 @@ -16,12 +16,17 @@ import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.maven.model.Dependency; +import org.apache.maven.model.DependencyManagement; import org.apache.maven.model.Model; public class MavenBuildFile extends BuildFile { - private AtomicReference modelRef = new AtomicReference<>(); + private static final Pattern PROPERTY_PATTERN = Pattern.compile("\\$\\{(.+)}"); + + private final AtomicReference modelRef = new AtomicReference<>(); public MavenBuildFile(final Path projectDirPath, final QuarkusPlatformDescriptor platformDescriptor) { super(projectDirPath, platformDescriptor); @@ -39,23 +44,46 @@ public void writeToDisk() throws IOException { } @Override - protected void addDependencyInBuildFile(AppArtifactCoords coords) throws IOException { - if (getModel() != null) { - final Dependency d = new Dependency(); - d.setGroupId(coords.getGroupId()); - d.setArtifactId(coords.getArtifactId()); + protected boolean addDependency(AppArtifactCoords coords, boolean managed) { + Model model = getModel(); + final Dependency d = new Dependency(); + d.setGroupId(coords.getGroupId()); + d.setArtifactId(coords.getArtifactId()); + if (!managed) { d.setVersion(coords.getVersion()); - // When classifier is empty, you get in the pom.xml - if (coords.getClassifier() != null && !coords.getClassifier().isEmpty()) { - d.setClassifier(coords.getClassifier()); + } + // When classifier is empty, you get in the pom.xml + if (coords.getClassifier() != null && !coords.getClassifier().isEmpty()) { + d.setClassifier(coords.getClassifier()); + } + d.setType(coords.getType()); + if ("pom".equalsIgnoreCase(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; + } + } else { + if (model.getDependencies() + .stream() + .noneMatch(thisDep -> d.getManagementKey().equals(thisDep.getManagementKey()))) { + model.addDependency(d); + return true; } - d.setType(coords.getType()); - getModel().addDependency(d); } + return false; } @Override - protected void removeDependencyFromBuildFile(AppArtifactKey key) throws IOException { + protected void removeDependency(AppArtifactKey key) throws IOException { if (getModel() != null) { getModel().getDependencies() .removeIf(d -> Objects.equals(toKey(d), key)); @@ -73,7 +101,7 @@ protected void refreshData() { } @Override - public String getProperty(String propertyName) throws IOException { + public String getProperty(String propertyName) { if (getModel() == null) { return null; } @@ -85,7 +113,7 @@ public BuildTool getBuildTool() { return BuildTool.MAVEN; } - private Model getModel() throws IOException { + private Model getModel() { return modelRef.updateAndGet(model -> { if (model == null) { try { @@ -102,4 +130,35 @@ private Model initModel() throws IOException { byte[] content = readProjectFile(BuildTool.MAVEN.getDependenciesFile()); return MojoUtils.readPom(new ByteArrayInputStream(content)); } + + /** + * Resolves dependencies containing property references in the GAV + */ + private Dependency toResolvedDependency(Dependency dependency) { + String resolvedGroupId = toResolvedProperty(dependency.getGroupId()); + String resolvedArtifactId = toResolvedProperty(dependency.getArtifactId()); + String resolvedVersion = toResolvedProperty(dependency.getVersion()); + if (!resolvedGroupId.equals(dependency.getGroupId()) + || !resolvedArtifactId.equals(dependency.getArtifactId()) + || !resolvedVersion.equals(dependency.getVersion())) { + Dependency newDep = dependency.clone(); + newDep.setGroupId(resolvedGroupId); + newDep.setArtifactId(resolvedArtifactId); + newDep.setVersion(resolvedVersion); + return newDep; + } + return dependency; + } + + /** + * Resolves properties as ${quarkus.platform.version} + */ + private String toResolvedProperty(String value) { + Matcher matcher = PROPERTY_PATTERN.matcher(value); + if (matcher.matches()) { + String property = getProperty(matcher.group(1)); + return property == null ? value : property; + } + return value; + } } diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/extensions/ExtensionInstallPlan.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/extensions/ExtensionInstallPlan.java new file mode 100644 index 0000000000000..bff4db0b86aa7 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/extensions/ExtensionInstallPlan.java @@ -0,0 +1,108 @@ +package io.quarkus.devtools.project.extensions; + +import io.quarkus.bootstrap.model.AppArtifactCoords; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +public class ExtensionInstallPlan { + + public static final ExtensionInstallPlan EMPTY = new ExtensionInstallPlan( + Collections.emptySet(), + Collections.emptySet(), + Collections.emptySet()); + + private final Set platforms; + private final Set managedExtensions; + private final Set independentExtensions; + + private ExtensionInstallPlan(Set platforms, + Set managedExtensions, + Set independentExtensions) { + this.platforms = platforms; + this.managedExtensions = managedExtensions; + this.independentExtensions = independentExtensions; + } + + public boolean isNotEmpty() { + return !this.platforms.isEmpty() || !this.managedExtensions.isEmpty() + || !this.independentExtensions.isEmpty(); + } + + /** + * @return a {@link Collection} of all extensions contained in this object + */ + public Collection toCollection() { + Set result = new LinkedHashSet<>(); + result.addAll(getPlatforms()); + result.addAll(getManagedExtensions()); + result.addAll(getIndependentExtensions()); + return result; + } + + /** + * @return Platforms (BOMs) to be added to the build descriptor + */ + public Collection getPlatforms() { + return platforms; + } + + /** + * @return Extensions that are included in the platforms returned in {@link #getPlatforms()}, + * therefore setting the version is not required. + */ + public Collection getManagedExtensions() { + return managedExtensions; + } + + /** + * @return Extensions that do not exist in any platform, the version MUST be set in the build descriptor + */ + public Collection getIndependentExtensions() { + return independentExtensions; + } + + @Override + public String toString() { + return "InstallRequest{" + + "platforms=" + platforms + + ", managedExtensions=" + managedExtensions + + ", independentExtensions=" + independentExtensions + + '}'; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private final Set platforms = new LinkedHashSet<>(); + private final Set extensionsInPlatforms = new LinkedHashSet<>(); + private final Set independentExtensions = new LinkedHashSet<>(); + + public ExtensionInstallPlan build() { + return new ExtensionInstallPlan(platforms, extensionsInPlatforms, independentExtensions); + } + + public Builder addIndependentExtension(AppArtifactCoords appArtifactCoords) { + this.independentExtensions.add(appArtifactCoords); + return this; + } + + public Builder addManagedExtension(AppArtifactCoords appArtifactCoords) { + this.extensionsInPlatforms.add(appArtifactCoords); + return this; + } + + public Builder addPlatform(AppArtifactCoords appArtifactCoords) { + this.platforms.add(appArtifactCoords); + return this; + } + + public boolean hasExtensionInPlatform() { + return !this.extensionsInPlatforms.isEmpty(); + } + } +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/extensions/ExtensionManager.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/extensions/ExtensionManager.java index 6f55c44103a05..6f0b700194464 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/extensions/ExtensionManager.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/extensions/ExtensionManager.java @@ -51,6 +51,21 @@ default boolean isInstalled(AppArtifactKey key) throws IOException { */ InstallResult install(Collection coords) throws IOException; + /** + * This is going to install/add all the specified extensions to the project build file(s). + * + *
+     *   - If the project Quarkus platform bom is not defined, an {@link IllegalStateException} will be thrown
+     *   - Extensions which are already installed will ALWAYS be skipped whatever the specified version
+     *   - The provided version will be used if wasn't already installed
+     * 
+ * + * @param request the list of {@link AppArtifactCoords} for the extensions to install + * @return the {@link InstallResult} + * @throws IOException if a problem occurs while reading/writing the project build file(s) + */ + InstallResult install(ExtensionInstallPlan request) throws IOException; + /** * This is going to uninstall/remove all the specified extensions from the project build file(s). * diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/DefaultExtensionRegistry.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/DefaultExtensionRegistry.java new file mode 100644 index 0000000000000..1f2441e105b62 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/DefaultExtensionRegistry.java @@ -0,0 +1,206 @@ +package io.quarkus.registry; + +import io.quarkus.bootstrap.model.AppArtifactCoords; +import io.quarkus.bootstrap.model.AppArtifactKey; +import io.quarkus.dependencies.Extension; +import io.quarkus.dependencies.ExtensionPredicate; +import io.quarkus.devtools.project.extensions.ExtensionInstallPlan; +import io.quarkus.platform.descriptor.QuarkusPlatformDescriptor; +import io.quarkus.registry.builder.RegistryBuilder; +import io.quarkus.registry.builder.URLRegistryBuilder; +import io.quarkus.registry.model.ArtifactKey; +import io.quarkus.registry.model.Extension.ExtensionRelease; +import io.quarkus.registry.model.Registry; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.maven.artifact.versioning.ComparableVersion; +import org.immutables.value.Value; + +/** + * This {@link ExtensionRegistry} implementation uses in-memory {@link Registry} + * objects to query the data. + */ +public class DefaultExtensionRegistry implements ExtensionRegistry { + + private final Registry registry; + + /** + * Create a {@link DefaultExtensionRegistry} out of a {@link Collection} of {@link URL}s + * + * @param urls urls used to lookup this registry + * @return a {@link DefaultExtensionRegistry} instance + * @throws IOException if any IO error occurs while reading each URL contents + */ + public static DefaultExtensionRegistry fromURLs(Collection urls) throws IOException { + Registry registry = new URLRegistryBuilder() + .addURLs(urls) + .build(); + return new DefaultExtensionRegistry(registry); + } + + /** + * Create a {@link DefaultExtensionRegistry} from a single {@link QuarkusPlatformDescriptor} + * + * @param platform the single platform + * @return a {@link DefaultExtensionRegistry} instance + */ + public static DefaultExtensionRegistry fromPlatform(QuarkusPlatformDescriptor platform) { + RegistryBuilder builder = new RegistryBuilder(); + builder.visitPlatform(platform); + return new DefaultExtensionRegistry(builder.build()); + } + + public DefaultExtensionRegistry(Registry registry) { + this.registry = Objects.requireNonNull(registry, "Registry cannot be null"); + } + + @Override + public Set getQuarkusCoreVersions() { + return registry.getCoreVersions().keySet().stream().map(ComparableVersion::toString).collect( + Collectors.toCollection(LinkedHashSet::new)); + } + + @Override + public Set getExtensionsByCoreVersion(String version) { + return list(version, ""); + } + + @Override + public Set list(String quarkusCore, String keyword) { + return listInternalExtensions(quarkusCore, keyword) + .stream() + .map(this::toQuarkusExtension) + .sorted(Comparator.comparing(Extension::getArtifactId)) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + @Override + public ExtensionInstallPlan planInstallation(String quarkusCore, Collection keywords) { + ExtensionInstallPlan.Builder builder = ExtensionInstallPlan.builder(); + boolean multipleKeywords = keywords.size() > 1; + for (String keyword : keywords) { + int countColons = StringUtils.countMatches(keyword, ":"); + // Check if it's just groupId:artifactId + if (countColons == 1) { + AppArtifactKey artifactKey = AppArtifactKey.fromString(keyword); + builder.addManagedExtension(new AppArtifactCoords(artifactKey, null)); + continue; + } else if (countColons > 1) { + // it's a gav + builder.addIndependentExtension(AppArtifactCoords.fromString(keyword)); + continue; + } + List tuples = listInternalExtensions(quarkusCore, keyword); + if (tuples.size() != 1 && multipleKeywords) { + // No extension found for this keyword. Return empty immediately + return ExtensionInstallPlan.EMPTY; + } + for (ExtensionReleaseTuple tuple : tuples) { + ArtifactKey id = tuple.getExtension().getId(); + String groupId = id.getGroupId(); + String artifactId = id.getArtifactId(); + String version = tuple.getRelease().getRelease().getVersion(); + AppArtifactCoords extensionCoords = new AppArtifactCoords(groupId, artifactId, version); + List platformCoords = tuple.getRelease().getPlatforms() + .stream() + .map(c -> new AppArtifactCoords( + c.getCoords().getId().getGroupId(), + c.getCoords().getId().getArtifactId(), + "pom", + c.getCoords().getVersion())) + .collect(Collectors.toList()); + if (platformCoords.isEmpty()) { + builder.addIndependentExtension(extensionCoords); + } else { + builder.addManagedExtension(extensionCoords); + for (AppArtifactCoords platformCoord : platformCoords) { + builder.addPlatform(platformCoord); + } + } + } + } + return builder.build(); + } + + private List listInternalExtensions(String quarkusCore, String keyword) { + List result = new ArrayList<>(); + ExtensionPredicate predicate = null; + if (keyword != null && !keyword.isEmpty()) { + predicate = new ExtensionPredicate(keyword); + } + for (io.quarkus.registry.model.Extension extension : registry.getExtensions()) { + for (ExtensionRelease extensionRelease : extension.getReleases()) { + if (quarkusCore.equals(extensionRelease.getRelease().getQuarkusCore())) { + ExtensionReleaseTuple tuple = ExtensionReleaseTuple.builder().extension(extension) + .release(extensionRelease).build(); + // If no filter is defined, just return the tuple + if (predicate == null) { + result.add(tuple); + } else { + Extension quarkusExtension = toQuarkusExtension(tuple); + // If there is an exact match, return only this + if (predicate.isExactMatch(quarkusExtension)) { + return Collections.singletonList(tuple); + } else if (predicate.test(quarkusExtension)) { + result.add(tuple); + } + } + break; + } + } + } + return result; + } + + private Extension toQuarkusExtension(ExtensionReleaseTuple tuple) { + io.quarkus.registry.model.Extension extension = tuple.getExtension(); + ExtensionRelease tupleRelease = tuple.getRelease(); + // Platforms may have metadata overriding the extension metadata + Map metadata = new HashMap<>(extension.getMetadata()); + tupleRelease.getPlatforms().stream() + .map(io.quarkus.registry.model.Extension.ExtensionPlatformRelease::getMetadata) + .findFirst() + .ifPresent(metadata::putAll); + ArtifactKey id = extension.getId(); + return new Extension() + .setGroupId(id.getGroupId()) + .setArtifactId(id.getArtifactId()) + .setVersion(tupleRelease.getRelease().getVersion()) + .setName(extension.getName()) + .setDescription(extension.getDescription()) + .setMetadata(metadata); + } + + /** + * Used in tests + */ + public Registry getRegistry() { + return registry; + } + + /** + * This exists only because ExtensionRelease does not accept a back reference to Extension + */ + @Value.Immutable + interface ExtensionReleaseTuple { + io.quarkus.registry.model.Extension getExtension(); + + ExtensionRelease getRelease(); + + static ImmutableExtensionReleaseTuple.Builder builder() { + return ImmutableExtensionReleaseTuple.builder(); + } + } +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/ExtensionRegistry.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/ExtensionRegistry.java new file mode 100644 index 0000000000000..a25dfb5172940 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/ExtensionRegistry.java @@ -0,0 +1,33 @@ +package io.quarkus.registry; + +import io.quarkus.dependencies.Extension; +import io.quarkus.devtools.project.extensions.ExtensionInstallPlan; +import java.util.Collection; + +public interface ExtensionRegistry { + + // Query methods + Collection getQuarkusCoreVersions(); + + Collection getExtensionsByCoreVersion(String version); + + /** + * Return the set of extensions that matches a given keyword + * + * @param quarkusCore the quarkus core this extension supports + * @param keyword the keyword to search + * @return a set of {@link Extension} objects or an empty set if not found + */ + Collection list(String quarkusCore, String keyword); + + /** + * What platforms and extensions do I need to add to my build descriptor? + * This method looks up the registry and provides a {@link ExtensionInstallPlan} containing this information. + * + * @param quarkusCore the quarkus core this extension supports + * @param keywords the keywords to lookup + * @return a {@link ExtensionInstallPlan} an object representing the necessary data to be added to a build descriptor + */ + ExtensionInstallPlan planInstallation(String quarkusCore, Collection keywords); + +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/RepositoryIndexer.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/RepositoryIndexer.java new file mode 100644 index 0000000000000..000dd6dd86ab8 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/RepositoryIndexer.java @@ -0,0 +1,45 @@ +package io.quarkus.registry; + +import io.quarkus.platform.descriptor.QuarkusPlatformDescriptor; +import io.quarkus.registry.catalog.model.Extension; +import io.quarkus.registry.catalog.model.Platform; +import io.quarkus.registry.catalog.model.Repository; +import io.quarkus.registry.catalog.spi.ArtifactResolver; +import io.quarkus.registry.catalog.spi.IndexVisitor; +import io.quarkus.registry.model.Release; +import java.io.IOException; +import java.util.Objects; + +/** + * Indexes a repository + */ +public class RepositoryIndexer { + + private final ArtifactResolver artifactResolver; + + public RepositoryIndexer(ArtifactResolver artifactResolver) { + this.artifactResolver = Objects.requireNonNull(artifactResolver, "Resolver cannot be null"); + } + + public void index(Repository repository, IndexVisitor visitor) throws IOException { + // Index Platforms + for (Platform platform : repository.getPlatforms()) { + for (Release release : platform.getReleases()) { + QuarkusPlatformDescriptor descriptor = artifactResolver.resolvePlatform(platform, release); + if (descriptor != null) { + visitor.visitPlatform(descriptor); + } + } + } + + // Index extensions + for (Extension extension : repository.getIndividualExtensions()) { + for (Release release : extension.getReleases()) { + io.quarkus.dependencies.Extension ext = artifactResolver.resolveExtension(extension, release); + if (ext != null) { + visitor.visitExtension(ext, release.getQuarkusCore()); + } + } + } + } +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/builder/ExtensionRegistryBuilder.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/builder/ExtensionRegistryBuilder.java new file mode 100644 index 0000000000000..8e1b62248fd40 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/builder/ExtensionRegistryBuilder.java @@ -0,0 +1,48 @@ +package io.quarkus.registry.builder; + +import io.quarkus.platform.descriptor.QuarkusPlatformDescriptor; +import io.quarkus.registry.DefaultExtensionRegistry; +import io.quarkus.registry.ExtensionRegistry; +import io.quarkus.registry.catalog.model.Extension; +import io.quarkus.registry.catalog.model.Platform; +import io.quarkus.registry.catalog.spi.ArtifactResolver; +import io.quarkus.registry.model.Registry; +import io.quarkus.registry.model.Release; +import java.io.IOException; + +/** + * Builds an {@link ExtensionRegistry} given the platforms and extensions resolved by the + * {@link ArtifactResolver} + */ +public class ExtensionRegistryBuilder { + + private final ArtifactResolver artifactResolver; + private final RegistryBuilder registryBuilder = new RegistryBuilder(); + + public ExtensionRegistryBuilder(ArtifactResolver artifactResolver) { + this.artifactResolver = artifactResolver; + } + + public ExtensionRegistryBuilder addPlatform(String groupId, String artifactId, String version) throws IOException { + Platform platform = Platform.builder().groupId(groupId).artifactId(artifactId).build(); + Release release = Release.builder().version(version).build(); + QuarkusPlatformDescriptor descriptor = artifactResolver.resolvePlatform(platform, release); + registryBuilder.visitPlatform(descriptor); + return this; + } + + public ExtensionRegistryBuilder addExtension(String groupId, String artifactId, String version, String quarkusCore) + throws IOException { + Extension extension = Extension.builder().groupId(groupId).artifactId(artifactId).build(); + Release release = Release.builder().version(version).build(); + io.quarkus.dependencies.Extension ext = artifactResolver.resolveExtension(extension, release); + registryBuilder.visitExtension(ext, quarkusCore); + return this; + } + + public ExtensionRegistry build() { + Registry registry = registryBuilder.build(); + return new DefaultExtensionRegistry(registry); + } + +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/builder/RegistryBuilder.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/builder/RegistryBuilder.java new file mode 100644 index 0000000000000..839e2103b656f --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/builder/RegistryBuilder.java @@ -0,0 +1,123 @@ +package io.quarkus.registry.builder; + +import io.quarkus.dependencies.Extension; +import io.quarkus.platform.descriptor.QuarkusPlatformDescriptor; +import io.quarkus.registry.catalog.spi.IndexVisitor; +import io.quarkus.registry.model.ArtifactCoords; +import io.quarkus.registry.model.ArtifactKey; +import io.quarkus.registry.model.Extension.ExtensionPlatformRelease; +import io.quarkus.registry.model.ImmutableArtifactCoords; +import io.quarkus.registry.model.ImmutableArtifactKey; +import io.quarkus.registry.model.ImmutableExtensionPlatformRelease; +import io.quarkus.registry.model.ImmutableRegistry; +import io.quarkus.registry.model.ModifiableExtension; +import io.quarkus.registry.model.ModifiableExtensionRelease; +import io.quarkus.registry.model.ModifiablePlatform; +import io.quarkus.registry.model.Registry; +import io.quarkus.registry.model.Release; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import org.apache.maven.artifact.versioning.ComparableVersion; +import org.immutables.value.Value; + +public class RegistryBuilder implements IndexVisitor { + + private final Map platforms = new LinkedHashMap<>(); + + private final Map extensions = new LinkedHashMap<>(); + + private final Map releases = new LinkedHashMap<>(); + + private final ImmutableRegistry.Builder registryBuilder = Registry.builder(); + + @Override + public void visitPlatform(QuarkusPlatformDescriptor platform) { + registryBuilder.putCoreVersions(new ComparableVersion(platform.getQuarkusVersion()), new HashMap<>()); + registryBuilder.addAllCategories(platform.getCategories()); + + ArtifactKey platformKey = ImmutableArtifactKey.of(platform.getBomGroupId(), platform.getBomArtifactId()); + ModifiablePlatform platformBuilder = platforms.computeIfAbsent(platformKey, + key -> ModifiablePlatform.create().setId(key)); + + platformBuilder.addReleases(Release.builder().version(platform.getBomVersion()) + .quarkusCore(platform.getQuarkusVersion()) + .build()); + + ArtifactCoords platformCoords = ImmutableArtifactCoords.of(platformKey, platform.getBomVersion()); + for (Extension extension : platform.getExtensions()) { + visitExtension(extension, platform.getQuarkusVersion(), platformCoords); + } + } + + @Override + public void visitExtension(Extension extension, String quarkusCore) { + visitExtension(extension, quarkusCore, null); + } + + private void visitExtension(Extension extension, String quarkusCore, ArtifactCoords platform) { + // Ignore unlisted extensions + if (extension.isUnlisted()) { + return; + } + registryBuilder.putCoreVersions(new ComparableVersion(quarkusCore), new HashMap<>()); + ArtifactKey extensionKey = ImmutableArtifactKey.of(extension.getGroupId(), extension.getArtifactId()); + ModifiableExtension extensionBuilder = extensions + .computeIfAbsent(extensionKey, key -> ModifiableExtension.create() + .setId(extensionKey) + .setName(Objects.toString(extension.getName(), extension.getArtifactId())) + .setDescription(extension.getDescription()) + .setMetadata(extension.getMetadata())); + ArtifactCoords coords = ImmutableArtifactCoords.of(extensionKey, extension.getVersion()); + ArtifactCoordsTuple key = ImmutableArtifactCoordsTuple.builder().coords(coords) + .quarkusVersion(quarkusCore) + .build(); + ModifiableExtensionRelease releaseBuilder = releases.computeIfAbsent(key, + appArtifactCoords -> ModifiableExtensionRelease.create() + .setRelease(Release.builder() + .version(appArtifactCoords.getCoords().getVersion()) + .quarkusCore(appArtifactCoords.getQuarkusVersion()) + .build())); + if (platform != null) { + Map metadata = diff(extensionBuilder.getMetadata(), extension.getMetadata()); + ExtensionPlatformRelease platformRelease = ImmutableExtensionPlatformRelease + .builder().coords(platform).metadata(metadata).build(); + releaseBuilder.addPlatforms(platformRelease); + } + } + + public Registry build() { + for (Map.Entry entry : releases.entrySet()) { + ArtifactCoordsTuple tuple = entry.getKey(); + ModifiableExtensionRelease extensionReleaseBuilder = entry.getValue(); + ArtifactKey key = tuple.getCoords().getId(); + ModifiableExtension extensionBuilder = extensions.get(key); + extensionBuilder.addReleases(extensionReleaseBuilder.toImmutable()); + } + extensions.values().stream().map(ModifiableExtension::toImmutable).forEach(registryBuilder::addExtensions); + platforms.values().stream().map(ModifiablePlatform::toImmutable).forEach(registryBuilder::addPlatforms); + return registryBuilder.build(); + } + + static Map diff(Map left, Map right) { + Map result = new HashMap<>(); + for (Map.Entry entry : right.entrySet()) { + Object value = left.get(entry.getKey()); + if (!entry.getValue().equals(value)) { + result.put(entry.getKey(), entry.getValue()); + } + } + return result; + } + + @Value.Immutable + public interface ArtifactCoordsTuple { + @Value.Parameter + ArtifactCoords getCoords(); + + @Value.Parameter + String getQuarkusVersion(); + } + +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/builder/URLRegistryBuilder.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/builder/URLRegistryBuilder.java new file mode 100644 index 0000000000000..b61fca0264e3f --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/builder/URLRegistryBuilder.java @@ -0,0 +1,51 @@ +package io.quarkus.registry.builder; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import io.quarkus.registry.model.ImmutableRegistry; +import io.quarkus.registry.model.Registry; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class URLRegistryBuilder { + + private final List urls = new ArrayList<>(); + + public URLRegistryBuilder addURL(URL url) { + urls.add(url); + return this; + } + + public URLRegistryBuilder addURLs(Collection urls) { + this.urls.addAll(urls); + return this; + } + + public Registry build() throws IOException { + if (urls.isEmpty()) { + throw new IllegalStateException("At least one URL must be specified"); + } + ObjectMapper mapper = new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE); + if (urls.size() == 1) { + // Just one + return mapper.readValue(urls.get(0), Registry.class); + } else { + ImmutableRegistry.Builder builder = Registry.builder(); + for (URL url : urls) { + Registry aRegistry = mapper.readValue(url, Registry.class); + builder.addAllCategories(aRegistry.getCategories()) + .addAllExtensions(aRegistry.getExtensions()) + .addAllPlatforms(aRegistry.getPlatforms()) + .putAllCoreVersions(aRegistry.getCoreVersions()); + } + return builder.build(); + } + + } +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/model/Extension.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/model/Extension.java new file mode 100644 index 0000000000000..bf7c365c59bd4 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/model/Extension.java @@ -0,0 +1,28 @@ +package io.quarkus.registry.catalog.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.quarkus.registry.model.Release; +import java.util.List; +import org.immutables.value.Value; + +/** + * An extension is a Maven dependency that can be added to a Quarkus project + */ +@Value.Immutable +@JsonDeserialize(as = ImmutableExtension.class) +public interface Extension { + + @JsonProperty("group-id") + String getGroupId(); + + @JsonProperty("artifact-id") + String getArtifactId(); + + @Value.Auxiliary + List getReleases(); + + static ImmutableExtension.Builder builder() { + return ImmutableExtension.builder(); + } +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/model/Platform.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/model/Platform.java new file mode 100644 index 0000000000000..fd6e96a254222 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/model/Platform.java @@ -0,0 +1,42 @@ +package io.quarkus.registry.catalog.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.quarkus.registry.model.Release; +import java.util.List; +import org.immutables.value.Value; + +/** + * A {@link Platform} holds a set of extensions + */ +@Value.Immutable +@JsonDeserialize(as = ImmutablePlatform.class) +public interface Platform { + + @JsonProperty("group-id") + String getGroupId(); + + @Value.Default + @JsonProperty("group-id-json") + @Value.Auxiliary + default String getGroupIdJson() { + return getGroupId(); + } + + @JsonProperty("artifact-id") + String getArtifactId(); + + @Value.Default + @JsonProperty("artifact-id-json") + @Value.Auxiliary + default String getArtifactIdJson() { + return getArtifactId(); + } + + @Value.Auxiliary + List getReleases(); + + static ImmutablePlatform.Builder builder() { + return ImmutablePlatform.builder(); + } +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/model/Repository.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/model/Repository.java new file mode 100644 index 0000000000000..54e74093cfa76 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/model/Repository.java @@ -0,0 +1,60 @@ +package io.quarkus.registry.catalog.model; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.type.CollectionType; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.immutables.value.Value.Immutable; + +@Immutable +@JsonDeserialize(as = ImmutableRepository.class) +public abstract class Repository { + + public abstract List getIndividualExtensions(); + + public abstract List getPlatforms(); + + /** + * - Match all files ending with '.json' inside an extensions directory + * - Match platforms.json + */ + public static Repository parse(Path rootPath, ObjectMapper mapper) { + ObjectReader reader = mapper.reader() + .with(mapper.getDeserializationConfig().with(PropertyNamingStrategy.KEBAB_CASE)); + return ImmutableRepository.builder() + .addAllPlatforms(parse(rootPath.resolve("platforms.json"), Platform.class, reader)) + .addAllIndividualExtensions(parse(rootPath.resolve("extensions"), Extension.class, reader)) + .build(); + } + + private static Set parse(Path root, Class type, ObjectReader reader) { + final Set result = new HashSet<>(); + if (Files.isDirectory(root)) { + try (DirectoryStream stream = Files.newDirectoryStream(root, "*.json")) { + for (Path path : stream) { + result.add(reader.forType(type).readValue(path.toFile())); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } else { + CollectionType collectionType = reader.getTypeFactory().constructCollectionType(List.class, type); + try { + result.addAll(reader.forType(collectionType).readValue(root.toFile())); + } catch (IOException e) { + e.printStackTrace(); + } + } + return result; + } + +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/spi/ArtifactResolver.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/spi/ArtifactResolver.java new file mode 100644 index 0000000000000..a2ed590778567 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/spi/ArtifactResolver.java @@ -0,0 +1,22 @@ +package io.quarkus.registry.catalog.spi; + +import io.quarkus.platform.descriptor.QuarkusPlatformDescriptor; +import io.quarkus.registry.catalog.model.Extension; +import io.quarkus.registry.catalog.model.Platform; +import io.quarkus.registry.model.Release; +import java.io.IOException; + +/** + * Resolves artifacts from the underlying artifact repositories + */ +public interface ArtifactResolver { + /** + * Resolve this specific platform + */ + QuarkusPlatformDescriptor resolvePlatform(Platform platform, Release release) throws IOException; + + /** + * Resolve this specific extension + */ + io.quarkus.dependencies.Extension resolveExtension(Extension extension, Release release) throws IOException; +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/spi/IndexVisitor.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/spi/IndexVisitor.java new file mode 100644 index 0000000000000..d2e9c5f32af5f --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/catalog/spi/IndexVisitor.java @@ -0,0 +1,11 @@ +package io.quarkus.registry.catalog.spi; + +import io.quarkus.dependencies.Extension; +import io.quarkus.platform.descriptor.QuarkusPlatformDescriptor; + +public interface IndexVisitor { + + void visitPlatform(QuarkusPlatformDescriptor platform); + + void visitExtension(Extension extension, String quarkusCore); +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/ArtifactCoords.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/ArtifactCoords.java new file mode 100644 index 0000000000000..dcb211448ddae --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/ArtifactCoords.java @@ -0,0 +1,16 @@ +package io.quarkus.registry.model; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.immutables.value.Value; + +@Value.Immutable +@JsonDeserialize(as = ImmutableArtifactCoords.class) +public interface ArtifactCoords { + @Value.Parameter + @JsonUnwrapped + ArtifactKey getId(); + + @Value.Parameter + String getVersion(); +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/ArtifactKey.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/ArtifactKey.java new file mode 100644 index 0000000000000..c38c3ef82b57a --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/ArtifactKey.java @@ -0,0 +1,17 @@ +package io.quarkus.registry.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.immutables.value.Value; + +@Value.Immutable +@JsonDeserialize(as = ImmutableArtifactKey.class) +public interface ArtifactKey { + @Value.Parameter + @JsonProperty("group-id") + String getGroupId(); + + @Value.Parameter + @JsonProperty("artifact-id") + String getArtifactId(); +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Extension.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Extension.java new file mode 100644 index 0000000000000..6e20e935ea8db --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Extension.java @@ -0,0 +1,58 @@ +package io.quarkus.registry.model; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import org.immutables.value.Value; + +@Value.Immutable +@Value.Modifiable +@JsonDeserialize(as = ImmutableExtension.class) +public interface Extension { + + @JsonUnwrapped + ArtifactKey getId(); + + @Value.Auxiliary + String getName(); + + @Value.Auxiliary + @Nullable + String getDescription(); + + @Value.Auxiliary + Map getMetadata(); + + @Value.Auxiliary + @Value.ReverseOrder + SortedSet getReleases(); + + @Value.Immutable + @Value.Modifiable + @JsonDeserialize(as = ImmutableExtensionRelease.class) + interface ExtensionRelease extends Comparable { + + @JsonUnwrapped + Release getRelease(); + + @Value.Auxiliary + Set getPlatforms(); + + @Override + default int compareTo(ExtensionRelease o) { + return getRelease().compareTo(o.getRelease()); + } + } + + @Value.Immutable + @JsonDeserialize(as = ImmutableExtensionPlatformRelease.class) + interface ExtensionPlatformRelease { + @JsonUnwrapped + ArtifactCoords getCoords(); + + @Value.Auxiliary + Map getMetadata(); + } +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Nullable.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Nullable.java new file mode 100644 index 0000000000000..223b4cb068841 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Nullable.java @@ -0,0 +1,4 @@ +package io.quarkus.registry.model; + +@interface Nullable { +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Platform.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Platform.java new file mode 100644 index 0000000000000..1c721254a74ee --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Platform.java @@ -0,0 +1,18 @@ +package io.quarkus.registry.model; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.Set; +import org.immutables.value.Value; + +@Value.Immutable +@Value.Modifiable +@JsonDeserialize(as = ImmutablePlatform.class) +public interface Platform { + + @JsonUnwrapped + ArtifactKey getId(); + + @Value.Auxiliary + Set getReleases(); +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Registry.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Registry.java new file mode 100644 index 0000000000000..7250c5af05692 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Registry.java @@ -0,0 +1,29 @@ +package io.quarkus.registry.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.quarkus.dependencies.Category; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import org.apache.maven.artifact.versioning.ComparableVersion; +import org.immutables.value.Value; + +@Value.Immutable +@JsonDeserialize(as = ImmutableRegistry.class) +public interface Registry { + + @JsonProperty("core-versions") + @Value.ReverseOrder + SortedMap> getCoreVersions(); + + Set getExtensions(); + + Set getPlatforms(); + + Set getCategories(); + + static ImmutableRegistry.Builder builder() { + return ImmutableRegistry.builder(); + } +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Release.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Release.java new file mode 100644 index 0000000000000..84e7650b56350 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/model/Release.java @@ -0,0 +1,37 @@ +package io.quarkus.registry.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.apache.maven.artifact.versioning.ComparableVersion; +import org.immutables.value.Value; + +@Value.Immutable +@JsonDeserialize(as = ImmutableRelease.class) +public interface Release extends Comparable { + + String getVersion(); + + @Nullable + @JsonProperty("quarkus-core") + String getQuarkusCore(); + + @Nullable + @JsonProperty("repository-url") + @Value.Auxiliary + String getRepositoryURL(); + + static ImmutableRelease.Builder builder() { + return ImmutableRelease.builder(); + } + + @Override + default int compareTo(Release o) { + int compare = new ComparableVersion(getVersion()) + .compareTo(new ComparableVersion(o.getVersion())); + if (compare == 0 && (getQuarkusCore() != null && o.getQuarkusCore() != null)) { + compare = new ComparableVersion(getQuarkusCore()) + .compareTo(new ComparableVersion(o.getQuarkusCore())); + } + return compare; + } +} diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/package-info.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/package-info.java new file mode 100644 index 0000000000000..0c6504e3ca885 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/registry/package-info.java @@ -0,0 +1,6 @@ +@Style(jdkOnly = true, visibility = PUBLIC, allowedClasspathAnnotations = { Override.class }) +package io.quarkus.registry; + +import static org.immutables.value.Value.Style.ImplementationVisibility.PUBLIC; + +import org.immutables.value.Value.Style; \ No newline at end of file diff --git a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/commands/AbstractAddExtensionsTest.java b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/commands/AbstractAddExtensionsTest.java index 32c0e4c1aba88..27492a7f844e8 100644 --- a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/commands/AbstractAddExtensionsTest.java +++ b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/commands/AbstractAddExtensionsTest.java @@ -1,6 +1,7 @@ package io.quarkus.devtools.commands; import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; import io.quarkus.devtools.commands.data.QuarkusCommandException; import io.quarkus.devtools.commands.data.QuarkusCommandOutcome; @@ -217,11 +218,11 @@ private void hasDependency(T project, String artifactId) { } private void hasDependency(T project, String groupId, String artifactId, String version) { - Assertions.assertTrue(countDependencyOccurrences(project, groupId, artifactId, version) > 0); + assertThat(countDependencyOccurrences(project, groupId, artifactId, version)).isNotZero(); } private void doesNotHaveDependency(T project, String artifactId) { - Assertions.assertTrue(countDependencyOccurrences(project, getPluginGroupId(), artifactId, null) == 0); + assertThat(countDependencyOccurrences(project, getPluginGroupId(), artifactId, null)).isZero(); } protected abstract T createProject() throws IOException, QuarkusCommandException; diff --git a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/commands/ListExtensionsTest.java b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/commands/ListExtensionsTest.java index 15dde3f7a6237..9eb5b3e14f56d 100644 --- a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/commands/ListExtensionsTest.java +++ b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/commands/ListExtensionsTest.java @@ -145,7 +145,7 @@ public void searchUnexpected() throws Exception { System.setOut(out); } final String output = baos.toString("UTF-8"); - Assertions.assertEquals(String.format("No extension found with this pattern%n"), output, + Assertions.assertEquals(String.format("No extension found with pattern 'unexpectedSearch'%n"), output, "search to unexpected extension must return a message"); } diff --git a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/buildfile/MavenBuildFileTest.java b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/buildfile/MavenBuildFileTest.java new file mode 100644 index 0000000000000..88f10e174789b --- /dev/null +++ b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/devtools/project/buildfile/MavenBuildFileTest.java @@ -0,0 +1,52 @@ +package io.quarkus.devtools.project.buildfile; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.quarkus.bootstrap.model.AppArtifactCoords; +import io.quarkus.maven.utilities.MojoUtils; +import io.quarkus.platform.descriptor.QuarkusPlatformDescriptor; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Properties; +import org.apache.maven.model.Dependency; +import org.apache.maven.model.DependencyManagement; +import org.apache.maven.model.Model; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +class MavenBuildFileTest { + + MavenBuildFile mavenBuildFile; + + @BeforeEach + void setUp(@TempDir Path projectDirPath) throws IOException { + Model model = new Model(); + Properties props = new Properties(); + props.setProperty("foo", "bar"); + props.setProperty("cheese", "pops"); + props.setProperty("one", "1"); + model.setProperties(props); + DependencyManagement depMan = new DependencyManagement(); + Dependency dependency = new Dependency(); + dependency.setGroupId("${foo}"); + dependency.setArtifactId("${cheese}"); + dependency.setVersion("${one}"); + dependency.setType("pom"); + dependency.setScope("import"); + depMan.addDependency(dependency); + model.setDependencyManagement(depMan); + Path pomPath = projectDirPath.resolve("pom.xml"); + MojoUtils.write(model, pomPath.toFile()); + QuarkusPlatformDescriptor mock = Mockito.mock(QuarkusPlatformDescriptor.class); + mavenBuildFile = new MavenBuildFile(projectDirPath, mock); + } + + @Test + void shouldNotAddManagedDependencyWithProperties() throws IOException { + AppArtifactCoords addedDep = new AppArtifactCoords("bar", "pops", "pom", "1"); + assertThat(mavenBuildFile.addDependency(addedDep, false)).isFalse(); + } + +} diff --git a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/RepositoryIndexerTest.java b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/RepositoryIndexerTest.java new file mode 100644 index 0000000000000..282e311b0d7e3 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/RepositoryIndexerTest.java @@ -0,0 +1,32 @@ +package io.quarkus.registry; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.dependencies.Extension; +import io.quarkus.platform.descriptor.QuarkusPlatformDescriptor; +import io.quarkus.registry.catalog.model.Repository; +import io.quarkus.registry.catalog.spi.IndexVisitor; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.jupiter.api.Test; + +class RepositoryIndexerTest { + @Test + void shouldVisitParsedElements() throws Exception { + Path rootPath = Paths.get("src/test/resources/registry/repository"); + assertThat(rootPath).exists(); + ObjectMapper mapper = new ObjectMapper(); + Repository repository = Repository.parse(rootPath, mapper); + RepositoryIndexer indexer = new RepositoryIndexer(new TestArtifactResolver()); + IndexVisitor mock = mock(IndexVisitor.class); + indexer.index(repository, mock); + verify(mock, atLeast(4)).visitPlatform(any(QuarkusPlatformDescriptor.class)); + verify(mock, atLeast(2)).visitExtension(any(Extension.class), anyString()); + } +} diff --git a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/TestArtifactResolver.java b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/TestArtifactResolver.java new file mode 100644 index 0000000000000..342ffd98595fe --- /dev/null +++ b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/TestArtifactResolver.java @@ -0,0 +1,74 @@ +package io.quarkus.registry; + +import io.quarkus.dependencies.Category; +import io.quarkus.dependencies.Extension; +import io.quarkus.platform.descriptor.QuarkusPlatformDescriptor; +import io.quarkus.platform.descriptor.ResourceInputStreamConsumer; +import io.quarkus.registry.catalog.model.Platform; +import io.quarkus.registry.catalog.spi.ArtifactResolver; +import io.quarkus.registry.model.Release; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.apache.maven.model.Dependency; + +public class TestArtifactResolver implements ArtifactResolver { + + @Override + public QuarkusPlatformDescriptor resolvePlatform(Platform platform, Release release) throws IOException { + return new QuarkusPlatformDescriptor() { + + @Override + public String getBomGroupId() { + return platform.getGroupId(); + } + + @Override + public String getBomArtifactId() { + return platform.getArtifactId(); + } + + @Override + public String getBomVersion() { + return release.getVersion(); + } + + @Override + public String getQuarkusVersion() { + return Objects.toString(release.getQuarkusCore(), "1.0.0"); + } + + @Override + public List getManagedDependencies() { + return Collections.emptyList(); + } + + @Override + public List getExtensions() { + return Collections.emptyList(); + } + + @Override + public List getCategories() { + return Collections.emptyList(); + } + + @Override + public String getTemplate(String name) { + return null; + } + + @Override + public T loadResource(String name, ResourceInputStreamConsumer consumer) throws IOException { + return null; + } + }; + } + + @Override + public Extension resolveExtension(io.quarkus.registry.catalog.model.Extension extension, Release release) + throws IOException { + return new Extension(extension.getGroupId(), extension.getArtifactId(), release.getVersion()); + } +} diff --git a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/builder/ExtensionRegistryBuilderTest.java b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/builder/ExtensionRegistryBuilderTest.java new file mode 100644 index 0000000000000..7f6ead47774a9 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/builder/ExtensionRegistryBuilderTest.java @@ -0,0 +1,38 @@ +package io.quarkus.registry.builder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.atMostOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.quarkus.registry.ExtensionRegistry; +import io.quarkus.registry.catalog.model.Extension; +import io.quarkus.registry.catalog.spi.ArtifactResolver; +import io.quarkus.registry.model.Release; +import java.io.IOException; +import org.junit.jupiter.api.Test; + +class ExtensionRegistryBuilderTest { + + @Test + void shouldResolveCamelDependencies() throws IOException { + String groupId = "org.apache.camel"; + String artifactId = "camel-quarkus"; + String version = "1.6.0.Final"; + + ArtifactResolver artifactResolver = mock(ArtifactResolver.class); + Release release = Release.builder().version(version).build(); + Extension extension = Extension.builder().groupId(groupId).artifactId(artifactId) + .addReleases(release).build(); + when(artifactResolver.resolveExtension(extension, release)) + .thenReturn(new io.quarkus.dependencies.Extension(groupId, artifactId, version)); + + ExtensionRegistry registry = new ExtensionRegistryBuilder(artifactResolver) + .addExtension(groupId, artifactId, version, version) + .build(); + verify(artifactResolver, atMostOnce()).resolveExtension(extension, release); + assertThat(registry.list(version, "camel")).isNotEmpty(); + } + +} diff --git a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/builder/RegistryBuilderTest.java b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/builder/RegistryBuilderTest.java new file mode 100644 index 0000000000000..9dd6e19b497e5 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/builder/RegistryBuilderTest.java @@ -0,0 +1,59 @@ +package io.quarkus.registry.builder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import io.quarkus.registry.RepositoryIndexer; +import io.quarkus.registry.TestArtifactResolver; +import io.quarkus.registry.catalog.model.Repository; +import io.quarkus.registry.model.Registry; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class RegistryBuilderTest { + + static Registry registry; + + @BeforeAll + static void setUp() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + Repository repository = Repository.parse(Paths.get("src/test/resources/registry/repository"), mapper); + RepositoryIndexer indexer = new RepositoryIndexer(new TestArtifactResolver()); + RegistryBuilder builder = new RegistryBuilder(); + indexer.index(repository, builder); + registry = builder.build(); + } + + @Test + void build() throws Exception { + assertThat(registry.getExtensions()).isNotEmpty(); + assertThat(registry.getPlatforms()).isNotEmpty(); + if (Boolean.getBoolean("generateTmpRegistry")) { + ObjectMapper mapper = new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE); + mapper.writeValue(new java.io.File("/tmp/registry.json"), registry); + } + } + + @Test + void diffMap() { + Map left = new HashMap<>(); + left.put("A", "1"); + left.put("B", "2"); + Map right = new HashMap<>(); + right.put("A", "1"); + right.put("B", "2"); + right.put("C", "3"); + Map result = RegistryBuilder.diff(left, right); + assertThat(result).doesNotContainKeys("A", "B").containsOnly(entry("C", "3")); + } + +} diff --git a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/builder/URLRegistryBuilderTest.java b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/builder/URLRegistryBuilderTest.java new file mode 100644 index 0000000000000..5cad5d6368c3a --- /dev/null +++ b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/builder/URLRegistryBuilderTest.java @@ -0,0 +1,24 @@ +package io.quarkus.registry.builder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.quarkus.registry.model.Registry; +import java.io.IOException; +import java.net.URL; +import org.junit.jupiter.api.Test; + +class URLRegistryBuilderTest { + + @Test + void shouldFailOnEmptyURLS() { + assertThrows(IllegalStateException.class, new URLRegistryBuilder()::build); + } + + @Test + void shouldReadRegistry() throws IOException { + URL url = getClass().getClassLoader().getResource("registry/registry.json"); + Registry registry = new URLRegistryBuilder().addURL(url).build(); + assertThat(registry.getCoreVersions()).isNotEmpty(); + } +} diff --git a/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/model/RepositoryTest.java b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/model/RepositoryTest.java new file mode 100644 index 0000000000000..35cd832fa3945 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/test/java/io/quarkus/registry/model/RepositoryTest.java @@ -0,0 +1,19 @@ +package io.quarkus.registry.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.registry.catalog.model.Repository; +import java.nio.file.Paths; +import org.junit.jupiter.api.Test; + +class RepositoryTest { + + @Test + void shouldParseRepository() throws Exception { + Repository repository = Repository.parse(Paths.get("src/test/resources/registry/repository"), new ObjectMapper()); + assertThat(repository).isNotNull(); + assertThat(repository.getPlatforms()).isNotEmpty(); + assertThat(repository.getIndividualExtensions()).isNotEmpty(); + } +} diff --git a/independent-projects/tools/devtools-common/src/test/resources/registry/registry.json b/independent-projects/tools/devtools-common/src/test/resources/registry/registry.json new file mode 100644 index 0000000000000..0ce8d67c77a05 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/test/resources/registry/registry.json @@ -0,0 +1,202 @@ +{ + "core-versions": { + "1.5.1.Final": {}, + "1.4.2.Final": {}, + "1.3.4.Final": {} + }, + "extensions" : [ + { + "group-id":"io.quarkus", + "artifact-id":"quarkus-resteasy", + "name":"RESTEasy JAX-RS", + "description":"REST endpoint framework implementing JAX-RS and more", + "metadata":{ + "short-name":"jax-rs", + "keywords":[ + "resteasy", + "jaxrs", + "web", + "rest" + ], + "guide":"https://quarkus.io/guides/rest-json", + "categories":[ + "web" + ], + "status":"stable" + }, + "releases":[ + { + "platforms":[ + { + "group-id":"io.quarkus", + "artifact-id":"quarkus-universe-bom", + "version":"1.5.1.Final" + }, + { + "group-id":"io.quarkus", + "artifact-id":"quarkus-bom", + "version":"1.5.1.Final" + } + ], + "version":"1.5.1.Final", + "quarkus-core":"1.5.1.Final" + }, + { + "platforms":[ + { + "group-id":"io.quarkus", + "artifact-id":"quarkus-universe-bom", + "version":"1.4.2.Final" + }, + { + "group-id":"io.quarkus", + "artifact-id":"quarkus-bom", + "version":"1.4.2.Final" + } + ], + "version":"1.4.2.Final", + "quarkus-core":"1.4.2.Final" + }, + { + "platforms":[ + { + "group-id":"io.quarkus", + "artifact-id":"quarkus-universe-bom", + "version":"1.3.4.Final" + }, + { + "group-id":"io.quarkus", + "artifact-id":"quarkus-bom", + "version":"1.3.4.Final" + } + ], + "version":"1.3.4.Final", + "quarkus-core":"1.3.4.Final" + } + ] + }, + { + "group-id":"io.quarkus", + "artifact-id":"quarkus-resteasy-jackson", + "name":"RESTEasy Jackson", + "description":"Jackson serialization support for RESTEasy", + "metadata":{ + "keywords":[ + "resteasy-jackson", + "jaxrs-json", + "resteasy-json", + "resteasy", + "jaxrs", + "json", + "jackson" + ], + "categories":[ + "web", + "serialization" + ], + "status":"stable" + }, + "releases":[ + { + "platforms":[ + { + "group-id":"io.quarkus", + "artifact-id":"quarkus-universe-bom", + "version":"1.5.1.Final" + }, + { + "group-id":"io.quarkus", + "artifact-id":"quarkus-bom", + "version":"1.5.1.Final" + } + ], + "version":"1.5.1.Final", + "quarkus-core":"1.5.1.Final" + }, + { + "platforms":[ + { + "group-id":"io.quarkus", + "artifact-id":"quarkus-universe-bom", + "version":"1.4.2.Final" + }, + { + "group-id":"io.quarkus", + "artifact-id":"quarkus-bom", + "version":"1.4.2.Final" + } + ], + "version":"1.4.2.Final", + "quarkus-core":"1.4.2.Final" + }, + { + "platforms":[ + { + "group-id":"io.quarkus", + "artifact-id":"quarkus-universe-bom", + "version":"1.3.4.Final" + }, + { + "group-id":"io.quarkus", + "artifact-id":"quarkus-bom", + "version":"1.3.4.Final" + } + ], + "version":"1.3.4.Final", + "quarkus-core":"1.3.4.Final" + } + ] + } + ], + "platforms":[ + { + "group-id":"io.quarkus", + "artifact-id":"quarkus-universe-bom", + "releases":[ + { + "version":"1.5.1.Final", + "quarkus-core":"1.5.1.Final" + }, + { + "version":"1.4.2.Final", + "quarkus-core":"1.4.2.Final" + }, + { + "version":"1.3.4.Final", + "quarkus-core":"1.3.4.Final" + } + ] + }, + { + "group-id":"io.quarkus", + "artifact-id":"quarkus-bom", + "releases":[ + { + "version":"1.5.1.Final", + "quarkus-core":"1.5.1.Final" + }, + { + "version":"1.4.2.Final", + "quarkus-core":"1.4.2.Final" + }, + { + "version":"1.3.4.Final", + "quarkus-core":"1.3.4.Final" + } + ] + } + ], + "categories":[ + { + "id":"web", + "name":"Web", + "description":"Everything you need for REST endpoints, HTTP and web formats like JSON", + "metadata":{ + "pinned":[ + "io.quarkus:quarkus-resteasy", + "io.quarkus:quarkus-resteasy-jackson" + ] + } + } + ] +} \ No newline at end of file diff --git a/independent-projects/tools/devtools-common/src/test/resources/registry/repository/extensions/jsf.json b/independent-projects/tools/devtools-common/src/test/resources/registry/repository/extensions/jsf.json new file mode 100644 index 0000000000000..7dedaad1cee99 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/test/resources/registry/repository/extensions/jsf.json @@ -0,0 +1,14 @@ +{ + "group-id": "org.apache.myfaces.core.extensions.quarkus", + "artifact-id": "myfaces-quarkus-runtime", + "releases": [ + { + "version": "2.3-next-M2", + "quarkus-core": "1.3.1.Final" + }, + { + "version": "2.3-next-M1", + "quarkus-core": "1.1.0.CR1" + } + ] +} \ No newline at end of file diff --git a/independent-projects/tools/devtools-common/src/test/resources/registry/repository/platforms.json b/independent-projects/tools/devtools-common/src/test/resources/registry/repository/platforms.json new file mode 100644 index 0000000000000..7d16491842aa3 --- /dev/null +++ b/independent-projects/tools/devtools-common/src/test/resources/registry/repository/platforms.json @@ -0,0 +1,30 @@ +[ + { + "group-id": "io.quarkus", + "artifact-id": "quarkus-universe-bom", + "releases": [ + { + "version": "1.5.2.Final" + }, + { + "version": "1.3.2.Final" + } + ] + }, + { + "group-id": "io.quarkus", + "artifact-id": "quarkus-bom", + "artifact-id-json": "quarkus-bom-descriptor-json", + "releases": [ + { + "version": "1.6.0.Final" + }, + { + "version": "1.5.2.Final" + }, + { + "version": "1.3.2.Final" + } + ] + } +] diff --git a/independent-projects/tools/platform-descriptor-api/pom.xml b/independent-projects/tools/platform-descriptor-api/pom.xml index 47d6a819637ea..de49dcf170fcb 100644 --- a/independent-projects/tools/platform-descriptor-api/pom.xml +++ b/independent-projects/tools/platform-descriptor-api/pom.xml @@ -23,5 +23,10 @@ junit-jupiter test + + org.assertj + assertj-core + test + diff --git a/independent-projects/tools/platform-descriptor-api/src/main/java/io/quarkus/dependencies/ExtensionPredicate.java b/independent-projects/tools/platform-descriptor-api/src/main/java/io/quarkus/dependencies/ExtensionPredicate.java new file mode 100644 index 0000000000000..2937990bf37e5 --- /dev/null +++ b/independent-projects/tools/platform-descriptor-api/src/main/java/io/quarkus/dependencies/ExtensionPredicate.java @@ -0,0 +1,143 @@ +package io.quarkus.dependencies; + +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * A {@link Predicate} implementation that will test a keyword against an {@link Extension} + */ +public class ExtensionPredicate implements Predicate { + + private final String q; + + public ExtensionPredicate(String keyword) { + this.q = Objects.requireNonNull(keyword, "keyword must not be null").trim().toLowerCase(); + } + + public static Predicate create(String keyword) { + return new ExtensionPredicate(keyword); + } + + @Override + public boolean test(Extension extension) { + if (extension.isUnlisted()) { + return false; + } + + String extensionName = Objects.toString(extension.getName(), ""); + String shortName = Objects.toString(extension.getShortName(), ""); + + // Try exact matches + if (isExactMatch(extension)) { + return true; + } + // Try short names + if (matchesShortName(extension, q)) { + return true; + } + // Partial matches on name, artifactId and short names + if (extensionName.toLowerCase().contains(q) + || extension.getArtifactId().toLowerCase().contains(q) + || shortName.toLowerCase().contains(q)) { + return true; + } + // find by labels + if (extension.labelsForMatching().contains(q)) { + return true; + } + // find by pattern + Pattern pattern = toRegex(q); + return pattern != null && (pattern.matcher(extensionName.toLowerCase()).matches() + || pattern.matcher(extension.getArtifactId().toLowerCase()).matches() + || pattern.matcher(shortName.toLowerCase()).matches() + || matchLabels(pattern, extension.getKeywords())); + } + + public boolean isExactMatch(Extension extension) { + String extensionName = Objects.toString(extension.getName(), ""); + // Try exact matches + if (extensionName.equalsIgnoreCase(q) || + matchesArtifactId(extension.getArtifactId(), q)) { + return true; + } + return false; + } + + private static boolean matchesShortName(Extension extension, String q) { + return q.equalsIgnoreCase(extension.getShortName()); + } + + private static boolean matchesArtifactId(String artifactId, String q) { + return artifactId.equalsIgnoreCase(q) || + artifactId.equalsIgnoreCase("quarkus-" + q); + } + + private static boolean matchLabels(Pattern pattern, List labels) { + boolean matches = false; + // if any label match it's ok + for (String label : labels) { + matches = matches || pattern.matcher(label.toLowerCase()).matches(); + } + return matches; + } + + private static Pattern toRegex(final String str) { + try { + String wildcardToRegex = wildcardToRegex(str); + if (wildcardToRegex != null && !wildcardToRegex.isEmpty()) { + return Pattern.compile(wildcardToRegex, Pattern.CASE_INSENSITIVE); + } + } catch (PatternSyntaxException e) { + //ignore it + } + return null; + } + + private static String wildcardToRegex(String wildcard) { + if (wildcard == null || wildcard.isEmpty()) { + return null; + } + // don't try with file match char in pattern + if (!(wildcard.contains("*") || wildcard.contains("?"))) { + return null; + } + StringBuilder s = new StringBuilder(wildcard.length()); + s.append("^.*"); + for (int i = 0, is = wildcard.length(); i < is; i++) { + char c = wildcard.charAt(i); + switch (c) { + case '*': + s.append(".*"); + break; + case '?': + s.append("."); + break; + case '^': // escape character in cmd.exe + s.append("\\"); + break; + // escape special regexp-characters + case '(': + case ')': + case '[': + case ']': + case '$': + case '.': + case '{': + case '}': + case '|': + case '\\': + s.append("\\"); + s.append(c); + break; + default: + s.append(c); + break; + } + } + s.append(".*$"); + return (s.toString()); + } +} diff --git a/independent-projects/tools/platform-descriptor-api/src/test/java/io/quarkus/dependencies/ExtensionPredicateTest.java b/independent-projects/tools/platform-descriptor-api/src/test/java/io/quarkus/dependencies/ExtensionPredicateTest.java new file mode 100644 index 0000000000000..7f9e27f90d6c0 --- /dev/null +++ b/independent-projects/tools/platform-descriptor-api/src/test/java/io/quarkus/dependencies/ExtensionPredicateTest.java @@ -0,0 +1,32 @@ +package io.quarkus.dependencies; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class ExtensionPredicateTest { + + @Test + void rejectUnlisted() { + ExtensionPredicate predicate = new ExtensionPredicate("foo"); + Extension extension = new Extension("g", "a", "v"); + extension.setUnlisted(true); + assertThat(predicate).rejects(extension); + } + + @Test + void acceptKeywordInArtifactId() { + ExtensionPredicate predicate = new ExtensionPredicate("foo"); + Extension extension = new Extension("g", "foo-bar", "1.0"); + assertThat(predicate).accepts(extension); + } + + @Test + void acceptKeywordInLabel() { + ExtensionPredicate predicate = new ExtensionPredicate("foo"); + Extension extension = new Extension("g", "a", "1.0"); + extension.setKeywords(new String[] { "foo", "bar" }); + assertThat(predicate).accepts(extension); + } + +} diff --git a/independent-projects/tools/pom.xml b/independent-projects/tools/pom.xml index c9dc81f25f937..26b696919536d 100644 --- a/independent-projects/tools/pom.xml +++ b/independent-projects/tools/pom.xml @@ -58,6 +58,7 @@ devtools-common utilities platform-descriptor-resolver-json + registry-descriptor-resolver @@ -71,6 +72,11 @@ minimal-json ${eclipse-minimal-json.version}
+ + io.quarkus + quarkus-bootstrap-app-model + ${quarkus.version} + io.quarkus quarkus-bootstrap-maven-resolver @@ -91,6 +97,11 @@ quarkus-platform-descriptor-api ${project.version} + + io.quarkus + quarkus-platform-descriptor-resolver-json + ${project.version} + io.quarkus.http quarkus-http-websockets-jsr diff --git a/independent-projects/tools/registry-descriptor-resolver/pom.xml b/independent-projects/tools/registry-descriptor-resolver/pom.xml new file mode 100644 index 0000000000000..fb74e966c60e2 --- /dev/null +++ b/independent-projects/tools/registry-descriptor-resolver/pom.xml @@ -0,0 +1,37 @@ + + + + quarkus-tools-parent + io.quarkus + 999-SNAPSHOT + + + 4.0.0 + + registry-descriptor-resolver + Quarkus - Dev tools - Registry descriptor resolver + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + io.quarkus + quarkus-platform-descriptor-resolver-json + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + + \ No newline at end of file diff --git a/independent-projects/tools/registry-descriptor-resolver/src/main/java/io/quarkus/registry/resolver/DefaultArtifactResolver.java b/independent-projects/tools/registry-descriptor-resolver/src/main/java/io/quarkus/registry/resolver/DefaultArtifactResolver.java new file mode 100644 index 0000000000000..d356ebcba598a --- /dev/null +++ b/independent-projects/tools/registry-descriptor-resolver/src/main/java/io/quarkus/registry/resolver/DefaultArtifactResolver.java @@ -0,0 +1,72 @@ +package io.quarkus.registry.resolver; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import io.quarkus.platform.descriptor.QuarkusPlatformDescriptor; +import io.quarkus.platform.descriptor.resolver.json.QuarkusJsonPlatformDescriptorResolver; +import io.quarkus.registry.catalog.model.Extension; +import io.quarkus.registry.catalog.model.Platform; +import io.quarkus.registry.catalog.spi.ArtifactResolver; +import io.quarkus.registry.model.Release; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.text.MessageFormat; +import java.util.Objects; + +public class DefaultArtifactResolver implements ArtifactResolver { + + private final ObjectMapper yamlReader; + private final QuarkusJsonPlatformDescriptorResolver resolver; + + private static final String MAVEN_CENTRAL = "https://repo1.maven.org/maven2/"; + + public DefaultArtifactResolver() { + this.yamlReader = new ObjectMapper(new YAMLFactory()) + .setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE); + this.resolver = QuarkusJsonPlatformDescriptorResolver.newInstance(); + } + + @Override + public QuarkusPlatformDescriptor resolvePlatform(Platform platform, Release release) throws IOException { + return resolver.resolveFromBom(platform.getGroupId(), platform.getArtifactId(), release.getVersion()); + } + + @Override + public io.quarkus.dependencies.Extension resolveExtension(Extension extension, Release release) throws IOException { + URL extensionJarURL = getExtensionJarURL(extension, release); + try { + return yamlReader.readValue(extensionJarURL, io.quarkus.dependencies.Extension.class); + } catch (FileNotFoundException e) { + // META-INF/quarkus-extension.yaml does not exist in JAR + return new io.quarkus.dependencies.Extension(extension.getGroupId(), extension.getArtifactId(), + release.getVersion()); + } + } + + static URL getPlatformJSONURL(Platform platform, Release release) { + try { + return new URL(MessageFormat.format("{0}{1}/{2}/{3}/{2}-{3}.json", + Objects.toString(release.getRepositoryURL(), MAVEN_CENTRAL), + platform.getGroupIdJson().replace('.', '/'), + platform.getArtifactIdJson(), + release.getVersion())); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Error while building JSON URL", e); + } + } + + static URL getExtensionJarURL(Extension extension, Release release) { + try { + return new URL(MessageFormat.format("jar:{0}{1}/{2}/{3}/{2}-{3}.jar!/META-INF/quarkus-extension.yaml", + Objects.toString(release.getRepositoryURL(), MAVEN_CENTRAL), + extension.getGroupId().replace('.', '/'), + extension.getArtifactId(), + release.getVersion())); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Error while building JSON URL", e); + } + } +} diff --git a/independent-projects/tools/registry-descriptor-resolver/src/test/java/io/quarkus/registry/resolver/DefaultArtifactResolverTest.java b/independent-projects/tools/registry-descriptor-resolver/src/test/java/io/quarkus/registry/resolver/DefaultArtifactResolverTest.java new file mode 100644 index 0000000000000..bfe3ba5ae9916 --- /dev/null +++ b/independent-projects/tools/registry-descriptor-resolver/src/test/java/io/quarkus/registry/resolver/DefaultArtifactResolverTest.java @@ -0,0 +1,51 @@ +package io.quarkus.registry.resolver; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.quarkus.platform.descriptor.QuarkusPlatformDescriptor; +import io.quarkus.registry.catalog.model.Extension; +import io.quarkus.registry.catalog.model.Platform; +import io.quarkus.registry.model.Release; +import java.io.IOException; +import java.net.URL; +import org.junit.jupiter.api.Test; + +class DefaultArtifactResolverTest { + + private DefaultArtifactResolver resolver = new DefaultArtifactResolver(); + + @Test + void shouldFormatURL() { + Release release = Release.builder().version("1.3.1.Final").build(); + Platform platform = Platform.builder() + .groupId("io.quarkus") + .artifactId("quarkus-universe-bom") + .addReleases(release).build(); + URL url = DefaultArtifactResolver.getPlatformJSONURL(platform, release); + assertThat(url).hasPath("/maven2/io/quarkus/quarkus-universe-bom/1.3.1.Final/quarkus-universe-bom-1.3.1.Final.json"); + } + + @Test + void shouldResolvePlatform() throws IOException { + Release release = Release.builder().version("1.3.1.Final").build(); + Platform platform = Platform.builder() + .groupId("io.quarkus") + .artifactId("quarkus-universe-bom") + .addReleases(release).build(); + QuarkusPlatformDescriptor descriptor = resolver.resolvePlatform(platform, release); + assertThat(descriptor).isNotNull(); + } + + @Test + void shouldResolveExtension() throws IOException { + Release release = Release.builder().version("1.3.1.Final").build(); + Extension extension = Extension.builder() + .groupId("io.quarkus") + .artifactId("quarkus-jgit") + .addReleases(release) + .build(); + io.quarkus.dependencies.Extension ext = resolver.resolveExtension(extension, release); + assertThat(ext).isNotNull(); + assertThat(ext.getArtifactId()).isEqualTo("quarkus-jgit"); + } +} diff --git a/independent-projects/tools/registry-descriptor-resolver/src/test/java/io/quarkus/registry/resolver/DefaultExtensionRegistryTest.java b/independent-projects/tools/registry-descriptor-resolver/src/test/java/io/quarkus/registry/resolver/DefaultExtensionRegistryTest.java new file mode 100644 index 0000000000000..23aa31c076240 --- /dev/null +++ b/independent-projects/tools/registry-descriptor-resolver/src/test/java/io/quarkus/registry/resolver/DefaultExtensionRegistryTest.java @@ -0,0 +1,95 @@ +package io.quarkus.registry.resolver; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import io.quarkus.bootstrap.model.AppArtifactCoords; +import io.quarkus.devtools.project.extensions.ExtensionInstallPlan; +import io.quarkus.registry.DefaultExtensionRegistry; +import io.quarkus.registry.RepositoryIndexer; +import io.quarkus.registry.builder.RegistryBuilder; +import io.quarkus.registry.catalog.model.Repository; +import io.quarkus.registry.model.Registry; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class DefaultExtensionRegistryTest { + + static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategy.KEBAB_CASE) + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + static DefaultExtensionRegistry extensionRegistry; + + @BeforeAll + static void setUp() throws IOException { + Repository repository = Repository.parse(Paths.get("src/test/resources/repository"), OBJECT_MAPPER); + RegistryBuilder registryBuilder = new RegistryBuilder(); + RepositoryIndexer indexer = new RepositoryIndexer(new DefaultArtifactResolver()); + indexer.index(repository, registryBuilder); + Registry registry = registryBuilder.build(); + extensionRegistry = new DefaultExtensionRegistry(registry); + } + + @Test + void serializationShouldKeepValues(@TempDir Path tmpDir) throws IOException { + File tmpFile = tmpDir.resolve("registry.json").toFile(); + OBJECT_MAPPER.writeValue(tmpFile, extensionRegistry.getRegistry()); + Registry newRegistry = OBJECT_MAPPER.readValue(tmpFile, Registry.class); + assertThat(newRegistry).isEqualToComparingFieldByField(extensionRegistry.getRegistry()); + } + + @Test + void shouldReturnFourQuarkusCoreVersions() { + assertThat(extensionRegistry.getQuarkusCoreVersions()).containsExactly( + "1.6.0.Final", + "1.5.2.Final", + "1.3.2.Final", + "1.3.1.Final", + "1.1.0.CR1"); + } + + @Test + void shouldLookupPlatformForDependentExtensionInQuarkusFinal() { + ExtensionInstallPlan result = extensionRegistry.planInstallation("1.3.2.Final", + Arrays.asList("quarkus-resteasy-qute", "quarkus-jgit")); + assertThat(result).isNotNull(); + assertThat(result.getPlatforms()).hasSize(2); + assertThat(result.getPlatforms()) + .extracting(AppArtifactCoords::getArtifactId) + .contains("quarkus-universe-bom", "quarkus-bom"); + assertThat(result.getManagedExtensions()).hasSize(2); + assertThat(result.getIndependentExtensions()).isEmpty(); + } + + @Test + void shouldLookupNoPlatformForIndependentExtension() { + ExtensionInstallPlan result = extensionRegistry.planInstallation("1.3.1.Final", + Collections.singletonList("myfaces-quarkus-runtime")); + assertThat(result).isNotNull(); + assertThat(result.getPlatforms()).isEmpty(); + assertThat(result.getManagedExtensions()).isEmpty(); + assertThat(result.getIndependentExtensions()).hasSize(1); + assertThat(result.getIndependentExtensions().iterator().next()) + .hasFieldOrPropertyWithValue("groupId", "org.apache.myfaces.core.extensions.quarkus") + .hasFieldOrPropertyWithValue("artifactId", "myfaces-quarkus-runtime") + .hasFieldOrPropertyWithValue("version", "2.3-next-M2"); + } + + @Test + void shouldLookupSinglePlatform() { + ExtensionInstallPlan result = extensionRegistry.planInstallation("1.3.2.Final", + Collections.singletonList("camel-quarkus-xstream")); + assertThat(result.getPlatforms()).hasSize(1); + + } + +} diff --git a/independent-projects/tools/registry-descriptor-resolver/src/test/resources/repository/extensions/jsf.json b/independent-projects/tools/registry-descriptor-resolver/src/test/resources/repository/extensions/jsf.json new file mode 100644 index 0000000000000..7dedaad1cee99 --- /dev/null +++ b/independent-projects/tools/registry-descriptor-resolver/src/test/resources/repository/extensions/jsf.json @@ -0,0 +1,14 @@ +{ + "group-id": "org.apache.myfaces.core.extensions.quarkus", + "artifact-id": "myfaces-quarkus-runtime", + "releases": [ + { + "version": "2.3-next-M2", + "quarkus-core": "1.3.1.Final" + }, + { + "version": "2.3-next-M1", + "quarkus-core": "1.1.0.CR1" + } + ] +} \ No newline at end of file diff --git a/independent-projects/tools/registry-descriptor-resolver/src/test/resources/repository/platforms.json b/independent-projects/tools/registry-descriptor-resolver/src/test/resources/repository/platforms.json new file mode 100644 index 0000000000000..7d16491842aa3 --- /dev/null +++ b/independent-projects/tools/registry-descriptor-resolver/src/test/resources/repository/platforms.json @@ -0,0 +1,30 @@ +[ + { + "group-id": "io.quarkus", + "artifact-id": "quarkus-universe-bom", + "releases": [ + { + "version": "1.5.2.Final" + }, + { + "version": "1.3.2.Final" + } + ] + }, + { + "group-id": "io.quarkus", + "artifact-id": "quarkus-bom", + "artifact-id-json": "quarkus-bom-descriptor-json", + "releases": [ + { + "version": "1.6.0.Final" + }, + { + "version": "1.5.2.Final" + }, + { + "version": "1.3.2.Final" + } + ] + } +]