Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the new selection algorithm #2785

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ public CommandResult execute(CommandInvocation commandInvocation) throws Command
File pomFile = new File(pom.getAbsolutePath());
AddExtensions project = new AddExtensions(new FileProjectWriter(pomFile.getParentFile()),
pomFile.getName());
project.addExtensions(Collections.singleton(extension));
AddExtensionResult result = project.addExtensions(Collections.singleton(extension));
if (!result.succeeded()) {
throw new CommandException("Unable to add an extension matching " + extension);
}
} catch (IOException e) {
e.printStackTrace();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class Extension {

private String simplifiedArtifactId;
private static final Pattern QUARKUS_PREFIX = Pattern.compile("^quarkus-");
private String shortName;

public Extension() {
// Use by mapper.
Expand Down Expand Up @@ -230,4 +231,16 @@ public boolean equals(Object obj) {
public String getGuide() {
return guide;
}

public String getShortName() {
if (shortName == null) {
return name;
}
return shortName;
}

public Extension setShortName(String shortName) {
this.shortName = shortName;
return this;
}
}
11 changes: 11 additions & 0 deletions devtools/common/src/main/filtered/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
{
"name": "Arc",
"shortName": "CDI",
"labels": [
"arc",
"cdi",
Expand Down Expand Up @@ -69,6 +70,7 @@
},
{
"name": "Hibernate ORM",
"shortName": "JPA",
"labels": [
"hibernate-orm",
"jpa",
Expand Down Expand Up @@ -105,6 +107,7 @@
},
{
"name": "Hibernate Validator",
"shortName": "bean validation",
"labels": [
"hibernate-validator",
"bean-validation",
Expand Down Expand Up @@ -242,6 +245,7 @@
},
{
"name": "RESTEasy",
"shortName": "jax-rs",
"labels": [
"resteasy",
"jaxrs",
Expand Down Expand Up @@ -289,6 +293,7 @@
},
{
"name": "SmallRye Context Propagation",
"shortName": "context propagation",
"labels": [
"smallrye-context-propagation",
"microprofile-context-propagation",
Expand All @@ -313,6 +318,7 @@
},
{
"name": "SmallRye Health",
"shortName": "health",
"labels": [
"smallrye-health",
"health-check",
Expand All @@ -338,6 +344,7 @@
},
{
"name": "SmallRye Metrics",
"shortName": "metrics",
"labels": [
"smallrye-metrics",
"metrics",
Expand Down Expand Up @@ -375,6 +382,7 @@
},
{
"name": "SmallRye Reactive Streams Operators",
"shortName": "reactive streams",
"labels": [
"smallrye-reactive-streams-operators",
"smallrye-reactive-streams",
Expand Down Expand Up @@ -412,6 +420,7 @@
},
{
"name": "SmallRye Reactive Messaging - Kafka Connector",
"shortName": "kafka",
"labels": [
"kafka",
"reactive-kafka"
Expand Down Expand Up @@ -462,6 +471,7 @@
},
{
"name": "Undertow",
"shortName": "servlet",
"labels": [
"undertow",
"servlet"
Expand All @@ -471,6 +481,7 @@
},
{
"name": "Undertow WebSockets",
"shortName": "websockets",
"labels": [
"undertow-websockets",
"undertow-websocket",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.quarkus.cli.commands;

public class AddExtensionResult {

private final boolean updated;
private final boolean succeeded;

public AddExtensionResult(boolean updated, boolean succeeded) {
this.updated = updated;
this.succeeded = succeeded;
}

public boolean isUpdated() {
return updated;
}

public boolean succeeded() {
return succeeded;
}
}
186 changes: 137 additions & 49 deletions devtools/common/src/main/java/io/quarkus/cli/commands/AddExtensions.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
Expand All @@ -18,9 +20,10 @@
import io.quarkus.maven.utilities.MojoUtils;

public class AddExtensions {
private static String OK = "\u2705";
private static String NOK = "\u274c";
private static String NOOP = "\uD83D\uDC4D";
private static final String OK = "\u2705";
private static final String NOK = "\u274c";
private static final String NOOP = "\uD83D\uDC4D";

private Model model;
private String pom;
private ProjectWriter writer;
Expand All @@ -31,57 +34,111 @@ public AddExtensions(final ProjectWriter writer, final String pom) throws IOExce
this.pom = pom;
}

public boolean addExtensions(final Set<String> extensions) throws IOException {
/**
* Selection algorithm.
*
* @param query the query
* @param extensions the extension list
* @param labelLookup whether or not the query must be tested against the labels of the extensions. Should
* be {@code false} by default.
* @return the list of matching candidates and whether or not a match has been found.
*/
static SelectionResult select(String query, List<Extension> extensions, boolean labelLookup) {
String q = query.trim().toLowerCase();

// Try exact matches
Set<Extension> matchesNameOrArtifactId = extensions.stream()
.filter(extension -> extension.getName().equalsIgnoreCase(q) || matchesArtifactId(extension.getArtifactId(), q))
.collect(Collectors.toSet());
if (matchesNameOrArtifactId.size() == 1) {
return new SelectionResult(matchesNameOrArtifactId, true);
}

// Try short names
Set<Extension> matchesShortName = extensions.stream().filter(extension -> matchesShortName(extension, q))
.collect(Collectors.toSet());

if (matchesShortName.size() == 1 && matchesNameOrArtifactId.isEmpty()) {
return new SelectionResult(matchesShortName, true);
}

// Partial matches on name, artifactId and short names
Set<Extension> partialMatches = extensions.stream().filter(extension -> extension.getName().toLowerCase().contains(q)
|| extension.getArtifactId().toLowerCase().contains(q)
|| extension.getShortName().toLowerCase().contains(q)).collect(Collectors.toSet());
// Even if we have a single partial match, if the name, artifactId and short names are ambiguous, so not
// consider it as a match.
if (partialMatches.size() == 1 && matchesNameOrArtifactId.isEmpty() && matchesShortName.isEmpty()) {
return new SelectionResult(partialMatches, true);
}

// find by labels
List<Extension> matchesLabels;
if (labelLookup) {
matchesLabels = extensions.stream()
.filter(extension -> extension.labels().contains(q)).collect(Collectors.toList());
} else {
matchesLabels = new ArrayList<>();
}

Set<Extension> candidates = new LinkedHashSet<>();
cescoffier marked this conversation as resolved.
Show resolved Hide resolved
candidates.addAll(matchesNameOrArtifactId);
candidates.addAll(matchesShortName);
candidates.addAll(partialMatches);
candidates.addAll(matchesLabels);
return new SelectionResult(candidates, 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);
}

public AddExtensionResult addExtensions(final Set<String> extensions) throws IOException {
if (extensions == null || extensions.isEmpty()) {
return false;
return new AddExtensionResult(false, true);
}

boolean updated = false;
boolean success = true;
List<Dependency> dependenciesFromBom = getDependenciesFromBom();

for (String dependency : extensions) {
List<Extension> matches = MojoUtils.loadExtensions().stream()
.filter(d -> {
boolean hasTag = d.labels().contains(dependency.trim().toLowerCase());
boolean machName = d.getName()
.toLowerCase()
.contains(dependency.trim().toLowerCase());
boolean matchArtifactId = d.getArtifactId()
.toLowerCase()
.contains(dependency.trim().toLowerCase());
return hasTag || machName || matchArtifactId;
})
.collect(Collectors.toList());

if (matches.size() > 1) {
StringBuilder sb = new StringBuilder();
sb.append(NOK)
.append(" Multiple extensions matching '" + dependency + "'");

matches.stream()
.forEach(extension -> sb.append(System.lineSeparator()).append(" * ")
.append(extension.managementKey()));
sb.append(System.lineSeparator()).append(" Be more specific e.g using the exact name or the full gav.");
System.out.println(sb);
} else if (matches.size() == 1) {
final Extension extension = matches.get(0);

if (!MojoUtils.hasDependency(model, extension.getGroupId(), extension.getArtifactId())) {
System.out.println(OK + " Adding extension " + extension.managementKey());
model.addDependency(extension
.toDependency(containsBOM(model) &&
isDefinedInBom(dependenciesFromBom, extension)));
updated = true;
} else {
System.out.println(NOOP + " Skipping extension " + extension.managementKey() + ": already present");
}
} else if (dependency.contains(":")) {
Dependency parsed = MojoUtils.parse(dependency);
System.out.println(OK + " Adding dependency " + parsed.getManagementKey());
model.addDependency(parsed);
updated = true;
List<Extension> registry = MojoUtils.loadExtensions();
for (String query : extensions) {

if (query.contains(":")) {
// GAV case.
updated = addExtensionAsGAV(query) || updated;
} else {
System.out.println(NOK + " Cannot find a dependency matching '" + dependency + "', maybe a typo?");
SelectionResult result = select(query, registry, false);
if (!result.matches()) {
StringBuilder sb = new StringBuilder();
// We have 3 cases, we can still have a single candidate, but the match is on label
// or we have several candidates, or none
Set<Extension> candidates = result.getExtensions();
if (candidates.isEmpty()) {
// No matches at all.
print(NOK + " Cannot find a dependency matching '" + query + "', maybe a typo?");
success = false;
} else {
sb.append(NOK).append(" Multiple extensions matching '").append(query).append("'");
result.getExtensions()
.forEach(extension -> sb.append(System.lineSeparator()).append(" * ")
.append(extension.managementKey()));
sb.append(System.lineSeparator())
.append(" Be more specific e.g using the exact name or the full GAV.");
print(sb.toString());
success = false;
}
} else { // Matches.
final Extension extension = result.getMatch();
// Don't set success to false even if the dependency is not added; as it's should be idempotent.
updated = addDependency(dependenciesFromBom, extension) || updated;
}
}
}

Expand All @@ -91,7 +148,38 @@ public boolean addExtensions(final Set<String> extensions) throws IOException {
writer.write(pom, pomOutputStream.toString("UTF-8"));
}

return updated;
return new AddExtensionResult(updated, success);
}

private boolean addDependency(List<Dependency> dependenciesFromBom, Extension extension) {
if (!MojoUtils.hasDependency(model, extension.getGroupId(), extension.getArtifactId())) {
print(OK + " Adding extension " + extension.managementKey());
model.addDependency(extension
.toDependency(containsBOM(model) &&
isDefinedInBom(dependenciesFromBom, extension)));
return true;
} else {
print(NOOP + " Skipping extension " + extension.managementKey() + ": already present");
return false;
}
}

private boolean addExtensionAsGAV(String query) {
Dependency parsed = MojoUtils.parse(query.trim().toLowerCase());
boolean alreadyThere = model.getDependencies().stream()
.anyMatch(d -> d.getManagementKey().equalsIgnoreCase(parsed.getManagementKey()));
if (!alreadyThere) {
print(OK + " Adding dependency " + parsed.getManagementKey());
model.addDependency(parsed);
return true;
} else {
print(NOOP + " Dependency " + parsed.getManagementKey() + " already in the pom.xml file - skipping");
return false;
}
}

private void print(String message) {
System.out.println(message);
}

private List<Dependency> getDependenciesFromBom() {
Expand All @@ -100,7 +188,7 @@ private List<Dependency> getDependenciesFromBom() {
.getDependencyManagement()
.getDependencies();
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
throw new IllegalStateException("Unable to read the BOM file: " + e.getMessage(), e);
}
}

Expand Down
Loading