From 68d975b4967900cacd1085ae6d2be135b867a43e Mon Sep 17 00:00:00 2001 From: David Pursehouse Date: Thu, 27 Jul 2017 22:14:38 +0100 Subject: [PATCH] ListPlugins: Add support for filtering with prefix/substring/regex Feature: Issue 6499 Change-Id: I7e54595dad56fc6a5e34bea55adee341077ad1fa --- Documentation/rest-api-plugins.txt | 95 +++++++++++++++++++ .../acceptance/api/plugin/PluginIT.java | 29 ++++++ .../extensions/api/plugins/Plugins.java | 30 ++++++ .../server/api/plugins/PluginsImpl.java | 3 + .../gerrit/server/plugins/ListPlugins.java | 60 +++++++++++- 5 files changed, 212 insertions(+), 5 deletions(-) diff --git a/Documentation/rest-api-plugins.txt b/Documentation/rest-api-plugins.txt index fb489d7f25..0f687bf99a 100644 --- a/Documentation/rest-api-plugins.txt +++ b/Documentation/rest-api-plugins.txt @@ -110,6 +110,74 @@ Query the first plugin in the plugin list: } ---- +Prefix(p):: +Limit the results to those plugins that start with the specified +prefix. ++ +The match is case sensitive. May not be used together with `m` or `r`. ++ +List all plugins that start with `delete`: ++ +.Request +---- + GET /plugins/?p=delete HTTP/1.0 +---- ++ +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + { + "delete-project": { + "id": "delete-project", + "index_url": "plugins/delete-project/", + "version": "2.9-SNAPSHOT" + } + } +---- ++ +E.g. this feature can be used by suggestion client UI's to limit results. + +Regex(r):: +Limit the results to those plugins that match the specified regex. ++ +Boundary matchers '^' and '$' are implicit. For example: the regex 'test.*' will +match any plugins that start with 'test' and regex '.*test' will match any +project that end with 'test'. ++ +The match is case sensitive. May not be used together with `m` or `p`. ++ +List all plugins that match regex `some.*plugin`: ++ +.Request +---- + GET /plugins/?r=some.*plugin HTTP/1.0 +---- ++ +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + { + "some-plugin": { + "id": "some-plugin", + "index_url": "plugins/some-plugin/", + "version": "2.9-SNAPSHOT" + }, + "some-other-plugin": { + "id": "some-other-plugin", + "index_url": "plugins/some-other-plugin/", + "version": "2.9-SNAPSHOT" + } + } + +---- Skip(S):: Skip the given number of plugins from the beginning of the list. @@ -138,6 +206,33 @@ Query the second plugin in the plugin list: } ---- +Substring(m):: +Limit the results to those plugins that match the specified substring. ++ +The match is case insensitive. May not be used together with `r` or `p`. ++ +List all plugins that match substring `project`: ++ +.Request +---- + GET /plugins/?m=project HTTP/1.0 +---- ++ +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + { + "delete-project": { + "id": "delete-project", + "index_url": "plugins/delete-project/", + "version": "2.9-SNAPSHOT" + } + } +---- [[install-plugin]] === Install Plugin diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/plugin/PluginIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/plugin/PluginIT.java index 61aba93e7d..4a7c34181d 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/plugin/PluginIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/plugin/PluginIT.java @@ -17,6 +17,7 @@ package com.google.gerrit.acceptance.api.plugin; import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.stream.Collectors.toList; +import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; import com.google.gerrit.acceptance.AbstractDaemonTest; @@ -27,6 +28,7 @@ import com.google.gerrit.extensions.api.plugins.PluginApi; import com.google.gerrit.extensions.api.plugins.Plugins.ListRequest; import com.google.gerrit.extensions.common.InstallPluginInput; import com.google.gerrit.extensions.common.PluginInfo; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestApiException; @@ -63,6 +65,24 @@ public class PluginIT extends AbstractDaemonTest { // With pagination assertPlugins(list().start(1).limit(2).get(), PLUGINS.subList(1, 3)); + // With prefix + assertPlugins(list().prefix("plugin-b").get(), ImmutableList.of("plugin-b")); + assertPlugins(list().prefix("PLUGIN-").get(), ImmutableList.of()); + + // With substring + assertPlugins(list().substring("lugin-").get(), PLUGINS); + assertPlugins(list().substring("lugin-").start(1).limit(2).get(), PLUGINS.subList(1, 3)); + + // With regex + assertPlugins(list().regex(".*in-b").get(), ImmutableList.of("plugin-b")); + assertPlugins(list().regex("plugin-.*").get(), PLUGINS); + assertPlugins(list().regex("plugin-.*").start(1).limit(2).get(), PLUGINS.subList(1, 3)); + + // Invalid match combinations + assertBadRequest(list().regex(".*in-b").substring("a")); + assertBadRequest(list().regex(".*in-b").prefix("a")); + assertBadRequest(list().substring(".*in-b").prefix("a")); + // Disable api = gApi.plugins().name("plugin-a"); api.disable(); @@ -99,4 +119,13 @@ public class PluginIT extends AbstractDaemonTest { List _actual = actual.stream().map(p -> p.id).collect(toList()); assertThat(_actual).containsExactlyElementsIn(expected); } + + private void assertBadRequest(ListRequest req) throws Exception { + try { + req.get(); + fail("Expected BadRequestException"); + } catch (BadRequestException e) { + // Expected + } + } } diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/plugins/Plugins.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/plugins/Plugins.java index ed0d7f6350..2828db5194 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/plugins/Plugins.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/plugins/Plugins.java @@ -35,6 +35,9 @@ public interface Plugins { private boolean all; private int limit; private int start; + private String substring; + private String prefix; + private String regex; public List get() throws RestApiException { Map map = getAsMap(); @@ -73,6 +76,33 @@ public interface Plugins { public int getStart() { return start; } + + public ListRequest substring(String substring) { + this.substring = substring; + return this; + } + + public String getSubstring() { + return substring; + } + + public ListRequest prefix(String prefix) { + this.prefix = prefix; + return this; + } + + public String getPrefix() { + return prefix; + } + + public ListRequest regex(String regex) { + this.regex = regex; + return this; + } + + public String getRegex() { + return regex; + } } /** diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/plugins/PluginsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/plugins/PluginsImpl.java index dead1ade94..75fb350bff 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/plugins/PluginsImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/plugins/PluginsImpl.java @@ -63,6 +63,9 @@ public class PluginsImpl implements Plugins { list.setAll(this.getAll()); list.setStart(this.getStart()); list.setLimit(this.getLimit()); + list.setMatchPrefix(this.getPrefix()); + list.setMatchSubstring(this.getSubstring()); + list.setMatchRegex(this.getRegex()); return list.apply(); } }; diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java index 517c743c27..0e514d63c3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java @@ -23,6 +23,7 @@ import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.extensions.annotations.RequiresCapability; import com.google.gerrit.extensions.common.PluginInfo; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.extensions.restapi.Url; @@ -31,9 +32,11 @@ import com.google.gson.reflect.TypeToken; import com.google.inject.Inject; import java.io.PrintWriter; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; +import java.util.regex.Pattern; import java.util.stream.Stream; import org.kohsuke.args4j.Option; @@ -45,6 +48,9 @@ public class ListPlugins implements RestReadView { private boolean all; private int limit; private int start; + private String matchPrefix; + private String matchSubstring; + private String matchRegex; @Deprecated @Option(name = "--format", usage = "(deprecated) output format") @@ -79,6 +85,31 @@ public class ListPlugins implements RestReadView { this.start = start; } + @Option( + name = "--prefix", + aliases = {"-p"}, + metaVar = "PREFIX", + usage = "match plugin prefix" + ) + public void setMatchPrefix(String matchPrefix) { + this.matchPrefix = matchPrefix; + } + + @Option( + name = "--match", + aliases = {"-m"}, + metaVar = "MATCH", + usage = "match plugin substring" + ) + public void setMatchSubstring(String matchSubstring) { + this.matchSubstring = matchSubstring; + } + + @Option(name = "-r", metaVar = "REGEX", usage = "match plugin regex") + public void setMatchRegex(String matchRegex) { + this.matchRegex = matchRegex; + } + @Inject protected ListPlugins(PluginLoader pluginLoader) { this.pluginLoader = pluginLoader; @@ -94,20 +125,33 @@ public class ListPlugins implements RestReadView { } @Override - public Object apply(TopLevelResource resource) { + public Object apply(TopLevelResource resource) throws BadRequestException { format = OutputFormat.JSON; return display(null); } - public SortedMap apply() { + public SortedMap apply() throws BadRequestException { format = OutputFormat.JSON; return display(null); } - public SortedMap display(@Nullable PrintWriter stdout) { + public SortedMap display(@Nullable PrintWriter stdout) + throws BadRequestException { SortedMap output = new TreeMap<>(); - Stream s = - Streams.stream(pluginLoader.getPlugins(all)).sorted(comparing(Plugin::getName)); + Stream s = Streams.stream(pluginLoader.getPlugins(all)); + if (matchPrefix != null) { + checkMatchOptions(matchSubstring == null && matchRegex == null); + s = s.filter(p -> p.getName().startsWith(matchPrefix)); + } else if (matchSubstring != null) { + checkMatchOptions(matchPrefix == null && matchRegex == null); + String substring = matchSubstring.toLowerCase(Locale.US); + s = s.filter(p -> p.getName().toLowerCase(Locale.US).contains(substring)); + } else if (matchRegex != null) { + checkMatchOptions(matchPrefix == null && matchSubstring == null); + Pattern pattern = Pattern.compile(matchRegex); + s = s.filter(p -> pattern.matcher(p.getName()).matches()); + } + s = s.sorted(comparing(Plugin::getName)); if (start > 0) { s = s.skip(start); } @@ -148,6 +192,12 @@ public class ListPlugins implements RestReadView { return null; } + private void checkMatchOptions(boolean cond) throws BadRequestException { + if (!cond) { + throw new BadRequestException("specify exactly one of p/m/r"); + } + } + public static PluginInfo toPluginInfo(Plugin p) { String id; String version;