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

[JENKINS-70822] Improve plugin manager search #9137

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
95 changes: 69 additions & 26 deletions core/src/main/java/hudson/PluginManager.java
Expand Up @@ -89,8 +89,10 @@
import java.security.CodeSource;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
Expand Down Expand Up @@ -1428,42 +1430,67 @@

@Restricted(NoExternalUse.class)
public HttpResponse doPluginsSearch(@QueryParameter String query, @QueryParameter Integer limit) {
String lQuery = query == null ? "" : query.toLowerCase(Locale.ENGLISH);

Check warning on line 1433 in core/src/main/java/hudson/PluginManager.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1433 is only partially covered, one branch is missing

List<JSONObject> plugins = new ArrayList<>();
for (UpdateSite site : Jenkins.get().getUpdateCenter().getSiteList()) {
List<JSONObject> sitePlugins = site.getAvailables().stream()
.filter(plugin -> {
if (query == null || query.isBlank()) {
return true;
}
return (plugin.name != null && plugin.name.toLowerCase().contains(query.toLowerCase())) ||
(plugin.title != null && plugin.title.toLowerCase().contains(query.toLowerCase())) ||
(plugin.excerpt != null && plugin.excerpt.toLowerCase().contains(query.toLowerCase())) ||
plugin.hasCategory(query) ||
plugin.getCategoriesStream()
.map(UpdateCenter::getCategoryDisplayName)
.anyMatch(category -> category != null && category.toLowerCase().contains(query.toLowerCase())) ||
plugin.hasWarnings() && query.equalsIgnoreCase("warning:");
})
.limit(Math.max(limit - plugins.size(), 1))
.sorted((o1, o2) -> {
String o1DisplayName = o1.getDisplayName();
if (o1.name.equalsIgnoreCase(query) ||
o1DisplayName.equalsIgnoreCase(query)) {
return -1;
}
String o2DisplayName = o2.getDisplayName();
if (o2.name.equalsIgnoreCase(query) || o2DisplayName.equalsIgnoreCase(query)) {
return 1;
}
if (o1.name.equals(o2.name)) {
return 0;
boolean matches = searchQueryMatches(query, plugin, lQuery);
if (matches) {
return true;
}
final int pop = Double.compare(o2.popularity, o1.popularity);
if (pop != 0) {
return pop; // highest popularity first

String[] tokenised = lQuery.split(" ");

if (tokenised.length > 1) {
return Arrays.stream(tokenised)
.anyMatch(q -> searchQueryMatchesLimited(q, plugin, q.toLowerCase()));
}
return o1DisplayName.compareTo(o2DisplayName);
return false;
})
.limit(Math.max(limit - plugins.size(), 1))
.sorted(
Comparator.comparingLong(plugin -> {
long score = 0;
if (plugin.name.equalsIgnoreCase(lQuery) ||

Check warning on line 1459 in core/src/main/java/hudson/PluginManager.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1459 is only partially covered, one branch is missing
plugin.getDisplayName().equalsIgnoreCase(lQuery)) {

Check warning on line 1460 in core/src/main/java/hudson/PluginManager.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1460 is only partially covered, one branch is missing
return -1000L;

Check warning on line 1461 in core/src/main/java/hudson/PluginManager.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 1461 is not covered by tests
}

String[] tokenised = lQuery.toLowerCase().split(" ");

// if greater than 1 word match in the title
if (tokenised.length > 1) {

Check warning on line 1467 in core/src/main/java/hudson/PluginManager.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1467 is only partially covered, one branch is missing
long o1Match = Arrays.stream(tokenised)
.filter(plugin.title.toLowerCase(Locale.ENGLISH)::contains)
.count();
if (o1Match > 1) {
score -= 100;
} else if (o1Match == 1) {
score -= 50;
}
}

if (plugin.isDeprecated()) {
score += 100;
} else {
if (plugin.popularity > 100000) {
score -= 40;
} else if (plugin.popularity > 10000) {
score -= 20;
} else if (plugin.popularity > 1000) {

Check warning on line 1485 in core/src/main/java/hudson/PluginManager.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1485 is only partially covered, one branch is missing
score -= 10;
}
}

return score;
})
)

.map(plugin -> {
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", plugin.name);
Expand Down Expand Up @@ -1531,6 +1558,22 @@
return hudson.util.HttpResponses.okJSON(mappedPlugins);
}

private static boolean searchQueryMatches(String query, UpdateSite.Plugin plugin, String lQuery) {
return (plugin.name != null && plugin.name.toLowerCase().contains(lQuery)) ||

Check warning on line 1562 in core/src/main/java/hudson/PluginManager.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1562 is only partially covered, 2 branches are missing
(plugin.title != null && plugin.title.toLowerCase().contains(lQuery)) ||

Check warning on line 1563 in core/src/main/java/hudson/PluginManager.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1563 is only partially covered, one branch is missing
(plugin.excerpt != null && plugin.excerpt.toLowerCase().contains(lQuery)) ||
plugin.hasCategory(query) ||

Check warning on line 1565 in core/src/main/java/hudson/PluginManager.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1565 is only partially covered, one branch is missing
plugin.getCategoriesStream()
.map(UpdateCenter::getCategoryDisplayName)
.anyMatch(category -> category != null && category.toLowerCase().contains(lQuery)) ||

Check warning on line 1568 in core/src/main/java/hudson/PluginManager.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1568 is only partially covered, 3 branches are missing
plugin.hasWarnings() && query.equalsIgnoreCase("warning:");

Check warning on line 1569 in core/src/main/java/hudson/PluginManager.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1569 is only partially covered, one branch is missing
}

private static boolean searchQueryMatchesLimited(String query, UpdateSite.Plugin plugin, String lQuery) {
return (plugin.name != null && plugin.name.toLowerCase().contains(lQuery)) ||

Check warning on line 1573 in core/src/main/java/hudson/PluginManager.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1573 is only partially covered, 2 branches are missing
(plugin.title != null && plugin.title.toLowerCase().contains(lQuery));
}

/**
* Get the list of all plugins - available and installed.
* @return The list of all plugins - available and installed.
Expand Down
3 changes: 1 addition & 2 deletions core/src/main/java/hudson/model/UpdateSite.java
Expand Up @@ -84,7 +84,6 @@
import net.sf.json.JSONException;
import net.sf.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.HttpResponse;
Expand Down Expand Up @@ -1573,7 +1572,7 @@ public Stream<String> getCategoriesStream() {
/**
* @since 2.40
*/
@Restricted(DoNotUse.class)
@Restricted(NoExternalUse.class)
public boolean hasWarnings() {
return !getWarnings().isEmpty();
}
Expand Down
46 changes: 46 additions & 0 deletions test/src/test/java/hudson/PluginManagerTest.java
Expand Up @@ -28,6 +28,8 @@
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.not;
Expand Down Expand Up @@ -81,6 +83,7 @@
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.servlet.ServletException;
import jenkins.ClassLoaderReflectionToolkit;
import jenkins.RestartRequiredException;
Expand Down Expand Up @@ -637,6 +640,49 @@ public void doNotThrowWithUnknownPlugins() throws Exception {
Assert.assertSame(FormValidation.Kind.OK, uc.getSite("default").updateDirectlyNow().kind);
}

@Test @Issue("JENKINS-64840")
public void filteringMultipleTokens() throws Exception {
assumeFalse("TODO: Implement this test for Windows", Functions.isWindows());
PersistedList<UpdateSite> sites = r.jenkins.getUpdateCenter().getSites();
sites.clear();
URL url = PluginManagerTest.class.getResource("/plugins/large-update-center.json");
UpdateSite site = new UpdateSite(UpdateCenter.ID_DEFAULT, url.toString());
sites.add(site);
assertEquals(FormValidation.ok(), site.updateDirectly(false).get());
assertNotNull(site.getData());

//Dummy plugin is found in the second site (should have worked before the fix)
JenkinsRule.JSONWebResponse response = r.getJSON("pluginManager/pluginsSearch?query=build%20token&limit=50");
JSONObject json = response.getJSONObject();
assertTrue(json.has("data"));
JSONArray data = json.getJSONArray("data");

List<String> artifactIdResults = data.stream()
.map(obj -> (JSONObject) obj)
.map(obj -> obj.getString("name"))
.collect(Collectors.toList());

assertThat(artifactIdResults, hasItem("build-token-root"));
String firstArtifact = artifactIdResults.get(0);

assertThat(firstArtifact, is("build-token-root"));

response = r.getJSON("pluginManager/pluginsSearch?query=Pipeline%20Groovy&limit=50");
json = response.getJSONObject();
assertTrue(json.has("data"));
data = json.getJSONArray("data");

artifactIdResults = data.stream()
.map(obj -> (JSONObject) obj)
.map(obj -> obj.getString("name"))
.collect(Collectors.toList());

assertThat(artifactIdResults, hasItem("workflow-cps"));

firstArtifact = artifactIdResults.get(0);
assertThat(firstArtifact, is("workflow-cps"));
}

@Test @Issue("JENKINS-64840")
public void searchMultipleUpdateSites() throws Exception {
assumeFalse("TODO: Implement this test for Windows", Functions.isWindows());
Expand Down
3 changes: 3 additions & 0 deletions test/src/test/resources/plugins/large-update-center.json

Large diffs are not rendered by default.