REST API /projects/$SUGGEST
The /projects/ URL now accepts a prefix string as part of the URL, making it usable by the suggestion client UI to limit results. With this change, the project suggestion service can be removed and replaced by the REST API. "GET /projects/platform/" will list all projects that start with the specified platform/ prefix. Change-Id: I6f8b9302166f59d03d58a85292cbf052e8419a78
This commit is contained in:
parent
e96071a099
commit
5cd0577828
@ -81,11 +81,17 @@ Line-feeds are escaped to allow ls-project to keep the
|
|||||||
the 'READ' access right is not assigned to the calling user
|
the 'READ' access right is not assigned to the calling user
|
||||||
account).
|
account).
|
||||||
|
|
||||||
|
--limit::
|
||||||
|
Cap the number of results to the first N matches.
|
||||||
|
|
||||||
HTTP
|
HTTP
|
||||||
----
|
----
|
||||||
This command is also available over HTTP, as `/projects/` for
|
This command is also available over HTTP, as `/projects/` for
|
||||||
anonymous access and `/a/projects/` for authenticated access.
|
anonymous access and `/a/projects/` for authenticated access.
|
||||||
Named options are available as query parameters.
|
Named options are available as query parameters. Results can
|
||||||
|
be limited to projects matching a prefix by supplying the prefix
|
||||||
|
as part of the URL, for example `/projects/external/` lists only
|
||||||
|
projects whose name start with the string `external/`.
|
||||||
|
|
||||||
Over HTTP the `json_compact` output format is assumed if the client
|
Over HTTP the `json_compact` output format is assumed if the client
|
||||||
explicitly asks for JSON using HTTP header `Accept: application/json`.
|
explicitly asks for JSON using HTTP header `Accept: application/json`.
|
||||||
@ -102,10 +108,16 @@ EXAMPLES
|
|||||||
List visible projects:
|
List visible projects:
|
||||||
=====
|
=====
|
||||||
$ ssh -p 29418 review.example.com gerrit ls-projects
|
$ ssh -p 29418 review.example.com gerrit ls-projects
|
||||||
|
platform/manifest
|
||||||
tools/gerrit
|
tools/gerrit
|
||||||
tools/gwtorm
|
tools/gwtorm
|
||||||
|
|
||||||
$ curl http://review.example.com/projects/
|
$ curl http://review.example.com/projects/
|
||||||
|
platform/manifest
|
||||||
|
tools/gerrit
|
||||||
|
tools/gwtorm
|
||||||
|
|
||||||
|
$ curl http://review.example.com/projects/tools/
|
||||||
tools/gerrit
|
tools/gerrit
|
||||||
tools/gwtorm
|
tools/gwtorm
|
||||||
=====
|
=====
|
||||||
|
@ -26,9 +26,6 @@ import java.util.List;
|
|||||||
|
|
||||||
@RpcImpl(version = Version.V2_0)
|
@RpcImpl(version = Version.V2_0)
|
||||||
public interface SuggestService extends RemoteJsonService {
|
public interface SuggestService extends RemoteJsonService {
|
||||||
void suggestProjectNameKey(String query, int limit,
|
|
||||||
AsyncCallback<List<Project.NameKey>> callback);
|
|
||||||
|
|
||||||
void suggestAccount(String query, Boolean enabled, int limit,
|
void suggestAccount(String query, Boolean enabled, int limit,
|
||||||
AsyncCallback<List<AccountInfo>> callback);
|
AsyncCallback<List<AccountInfo>> callback);
|
||||||
|
|
||||||
|
@ -16,8 +16,11 @@ package com.google.gerrit.client.projects;
|
|||||||
|
|
||||||
import com.google.gerrit.reviewdb.client.Project;
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
import com.google.gwt.core.client.JavaScriptObject;
|
import com.google.gwt.core.client.JavaScriptObject;
|
||||||
|
import com.google.gwt.user.client.ui.SuggestOracle;
|
||||||
|
|
||||||
public class ProjectInfo extends JavaScriptObject {
|
public class ProjectInfo
|
||||||
|
extends JavaScriptObject
|
||||||
|
implements SuggestOracle.Suggestion {
|
||||||
public final Project.NameKey name_key() {
|
public final Project.NameKey name_key() {
|
||||||
return new Project.NameKey(name());
|
return new Project.NameKey(name());
|
||||||
}
|
}
|
||||||
@ -25,6 +28,19 @@ public class ProjectInfo extends JavaScriptObject {
|
|||||||
public final native String name() /*-{ return this.name; }-*/;
|
public final native String name() /*-{ return this.name; }-*/;
|
||||||
public final native String description() /*-{ return this.description; }-*/;
|
public final native String description() /*-{ return this.description; }-*/;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final String getDisplayString() {
|
||||||
|
if (description() != null) {
|
||||||
|
return name() + " (" + description() + ")";
|
||||||
|
}
|
||||||
|
return name();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final String getReplacementString() {
|
||||||
|
return name();
|
||||||
|
}
|
||||||
|
|
||||||
protected ProjectInfo() {
|
protected ProjectInfo() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ package com.google.gerrit.client.projects;
|
|||||||
import com.google.gerrit.client.rpc.NativeMap;
|
import com.google.gerrit.client.rpc.NativeMap;
|
||||||
import com.google.gerrit.client.rpc.RestApi;
|
import com.google.gerrit.client.rpc.RestApi;
|
||||||
import com.google.gwtjsonrpc.common.AsyncCallback;
|
import com.google.gwtjsonrpc.common.AsyncCallback;
|
||||||
|
import com.google.gwt.http.client.URL;
|
||||||
|
|
||||||
/** Projects available from {@code /projects/}. */
|
/** Projects available from {@code /projects/}. */
|
||||||
public class ProjectMap extends NativeMap<ProjectInfo> {
|
public class ProjectMap extends NativeMap<ProjectInfo> {
|
||||||
@ -36,6 +37,14 @@ public class ProjectMap extends NativeMap<ProjectInfo> {
|
|||||||
.send(NativeMap.copyKeysIntoChildren(callback));
|
.send(NativeMap.copyKeysIntoChildren(callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void suggest(String prefix, int limit, AsyncCallback<ProjectMap> cb) {
|
||||||
|
new RestApi("/projects/" + URL.encode(prefix).replaceAll("[?]", "%3F"))
|
||||||
|
.addParameterRaw("type", "ALL")
|
||||||
|
.addParameter("n", limit)
|
||||||
|
.addParameterTrue("d") // description
|
||||||
|
.send(NativeMap.copyKeysIntoChildren(cb));
|
||||||
|
}
|
||||||
|
|
||||||
protected ProjectMap() {
|
protected ProjectMap() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,49 +15,25 @@
|
|||||||
package com.google.gerrit.client.ui;
|
package com.google.gerrit.client.ui;
|
||||||
|
|
||||||
import com.google.gerrit.client.RpcStatus;
|
import com.google.gerrit.client.RpcStatus;
|
||||||
|
import com.google.gerrit.client.projects.ProjectMap;
|
||||||
import com.google.gerrit.client.rpc.GerritCallback;
|
import com.google.gerrit.client.rpc.GerritCallback;
|
||||||
import com.google.gerrit.reviewdb.client.Project;
|
|
||||||
import com.google.gwt.user.client.ui.SuggestOracle;
|
|
||||||
import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
|
import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/** Suggestion Oracle for Project.NameKey entities. */
|
/** Suggestion Oracle for Project.NameKey entities. */
|
||||||
public class ProjectNameSuggestOracle extends HighlightSuggestOracle {
|
public class ProjectNameSuggestOracle extends HighlightSuggestOracle {
|
||||||
@Override
|
@Override
|
||||||
public void onRequestSuggestions(final Request req, final Callback callback) {
|
public void onRequestSuggestions(final Request req, final Callback callback) {
|
||||||
RpcStatus.hide(new Runnable() {
|
RpcStatus.hide(new Runnable() {
|
||||||
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
SuggestUtil.SVC.suggestProjectNameKey(req.getQuery(), req.getLimit(),
|
ProjectMap.suggest(req.getQuery(), req.getLimit(),
|
||||||
new GerritCallback<List<Project.NameKey>>() {
|
new GerritCallback<ProjectMap>() {
|
||||||
public void onSuccess(final List<Project.NameKey> result) {
|
@Override
|
||||||
final ArrayList<ProjectNameSuggestion> r =
|
public void onSuccess(ProjectMap map) {
|
||||||
new ArrayList<ProjectNameSuggestion>(result.size());
|
callback.onSuggestionsReady(req, new Response(map.values().asList()));
|
||||||
for (final Project.NameKey p : result) {
|
|
||||||
r.add(new ProjectNameSuggestion(p));
|
|
||||||
}
|
|
||||||
callback.onSuggestionsReady(req, new Response(r));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ProjectNameSuggestion implements
|
|
||||||
SuggestOracle.Suggestion {
|
|
||||||
private final Project.NameKey key;
|
|
||||||
|
|
||||||
ProjectNameSuggestion(final Project.NameKey k) {
|
|
||||||
key = k;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDisplayString() {
|
|
||||||
return key.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getReplacementString() {
|
|
||||||
return key.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ class UrlModule extends ServletModule {
|
|||||||
|
|
||||||
filter("/a/*").through(RequireIdentifiedUserFilter.class);
|
filter("/a/*").through(RequireIdentifiedUserFilter.class);
|
||||||
serveRegex("^/(?:a/)?accounts/self/capabilities$").with(AccountCapabilitiesServlet.class);
|
serveRegex("^/(?:a/)?accounts/self/capabilities$").with(AccountCapabilitiesServlet.class);
|
||||||
serveRegex("^/(a/)?projects/$").with(ListProjectsServlet.class);
|
serveRegex("^/(?:a/)?projects/(.*)?$").with(ListProjectsServlet.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Key<HttpServlet> notFound() {
|
private Key<HttpServlet> notFound() {
|
||||||
|
@ -39,8 +39,6 @@ import com.google.gerrit.server.patch.AddReviewer;
|
|||||||
import com.google.gerrit.server.project.ChangeControl;
|
import com.google.gerrit.server.project.ChangeControl;
|
||||||
import com.google.gerrit.server.project.NoSuchChangeException;
|
import com.google.gerrit.server.project.NoSuchChangeException;
|
||||||
import com.google.gerrit.server.project.NoSuchProjectException;
|
import com.google.gerrit.server.project.NoSuchProjectException;
|
||||||
import com.google.gerrit.server.project.ProjectCache;
|
|
||||||
import com.google.gerrit.server.project.ProjectControl;
|
|
||||||
import com.google.gwtjsonrpc.common.AsyncCallback;
|
import com.google.gwtjsonrpc.common.AsyncCallback;
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
@ -60,8 +58,6 @@ class SuggestServiceImpl extends BaseServiceImplementation implements
|
|||||||
private static final String MAX_SUFFIX = "\u9fa5";
|
private static final String MAX_SUFFIX = "\u9fa5";
|
||||||
|
|
||||||
private final Provider<ReviewDb> reviewDbProvider;
|
private final Provider<ReviewDb> reviewDbProvider;
|
||||||
private final ProjectControl.Factory projectControlFactory;
|
|
||||||
private final ProjectCache projectCache;
|
|
||||||
private final AccountCache accountCache;
|
private final AccountCache accountCache;
|
||||||
private final GroupControl.Factory groupControlFactory;
|
private final GroupControl.Factory groupControlFactory;
|
||||||
private final GroupMembers.Factory groupMembersFactory;
|
private final GroupMembers.Factory groupMembersFactory;
|
||||||
@ -74,8 +70,7 @@ class SuggestServiceImpl extends BaseServiceImplementation implements
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SuggestServiceImpl(final Provider<ReviewDb> schema,
|
SuggestServiceImpl(final Provider<ReviewDb> schema,
|
||||||
final ProjectControl.Factory projectControlFactory,
|
final AccountCache accountCache,
|
||||||
final ProjectCache projectCache, final AccountCache accountCache,
|
|
||||||
final GroupControl.Factory groupControlFactory,
|
final GroupControl.Factory groupControlFactory,
|
||||||
final GroupMembers.Factory groupMembersFactory,
|
final GroupMembers.Factory groupMembersFactory,
|
||||||
final Provider<CurrentUser> currentUser,
|
final Provider<CurrentUser> currentUser,
|
||||||
@ -85,8 +80,6 @@ class SuggestServiceImpl extends BaseServiceImplementation implements
|
|||||||
@GerritServerConfig final Config cfg, final GroupCache groupCache) {
|
@GerritServerConfig final Config cfg, final GroupCache groupCache) {
|
||||||
super(schema, currentUser);
|
super(schema, currentUser);
|
||||||
this.reviewDbProvider = schema;
|
this.reviewDbProvider = schema;
|
||||||
this.projectControlFactory = projectControlFactory;
|
|
||||||
this.projectCache = projectCache;
|
|
||||||
this.accountCache = accountCache;
|
this.accountCache = accountCache;
|
||||||
this.groupControlFactory = groupControlFactory;
|
this.groupControlFactory = groupControlFactory;
|
||||||
this.groupMembersFactory = groupMembersFactory;
|
this.groupMembersFactory = groupMembersFactory;
|
||||||
@ -111,28 +104,6 @@ class SuggestServiceImpl extends BaseServiceImplementation implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void suggestProjectNameKey(final String query, final int limit,
|
|
||||||
final AsyncCallback<List<Project.NameKey>> callback) {
|
|
||||||
final int max = 10;
|
|
||||||
final int n = limit <= 0 ? max : Math.min(limit, max);
|
|
||||||
|
|
||||||
final List<Project.NameKey> r = new ArrayList<Project.NameKey>(n);
|
|
||||||
for (final Project.NameKey nameKey : projectCache.byName(query)) {
|
|
||||||
final ProjectControl ctl;
|
|
||||||
try {
|
|
||||||
ctl = projectControlFactory.validateFor(nameKey);
|
|
||||||
} catch (NoSuchProjectException e) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
r.add(ctl.getProject().getNameKey());
|
|
||||||
if (r.size() == n) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
callback.onSuccess(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
private interface VisibilityControl {
|
private interface VisibilityControl {
|
||||||
boolean isVisible(Account account) throws OrmException;
|
boolean isVisible(Account account) throws OrmException;
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
package com.google.gerrit.httpd.rpc.project;
|
package com.google.gerrit.httpd.rpc.project;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
import com.google.gerrit.httpd.RestApiServlet;
|
import com.google.gerrit.httpd.RestApiServlet;
|
||||||
import com.google.gerrit.server.OutputFormat;
|
import com.google.gerrit.server.OutputFormat;
|
||||||
import com.google.gerrit.server.project.ListProjects;
|
import com.google.gerrit.server.project.ListProjects;
|
||||||
@ -23,6 +24,7 @@ import com.google.inject.Singleton;
|
|||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
@ -43,6 +45,9 @@ public class ListProjectsServlet extends RestApiServlet {
|
|||||||
protected void doGet(HttpServletRequest req, HttpServletResponse res)
|
protected void doGet(HttpServletRequest req, HttpServletResponse res)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
ListProjects impl = factory.get();
|
ListProjects impl = factory.get();
|
||||||
|
if (!Strings.isNullOrEmpty(req.getPathInfo())) {
|
||||||
|
impl.setMatchPrefix(URLDecoder.decode(req.getPathInfo(), "UTF-8"));
|
||||||
|
}
|
||||||
if (acceptsJson(req)) {
|
if (acceptsJson(req)) {
|
||||||
impl.setFormat(OutputFormat.JSON_COMPACT);
|
impl.setFormat(OutputFormat.JSON_COMPACT);
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ package com.google.gerrit.server.project;
|
|||||||
|
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import com.google.gerrit.reviewdb.client.Project;
|
import com.google.gerrit.reviewdb.client.Project;
|
||||||
|
import com.google.gerrit.reviewdb.client.Project.NameKey;
|
||||||
import com.google.gerrit.server.CurrentUser;
|
import com.google.gerrit.server.CurrentUser;
|
||||||
import com.google.gerrit.server.OutputFormat;
|
import com.google.gerrit.server.OutputFormat;
|
||||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||||
@ -100,6 +101,11 @@ public class ListProjects {
|
|||||||
@Option(name = "--all", usage = "display all projects that are accessible by the calling user")
|
@Option(name = "--all", usage = "display all projects that are accessible by the calling user")
|
||||||
private boolean all;
|
private boolean all;
|
||||||
|
|
||||||
|
@Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of projects to list")
|
||||||
|
private int limit;
|
||||||
|
|
||||||
|
private String matchPrefix;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
protected ListProjects(CurrentUser currentUser, ProjectCache projectCache,
|
protected ListProjects(CurrentUser currentUser, ProjectCache projectCache,
|
||||||
GitRepositoryManager repoManager,
|
GitRepositoryManager repoManager,
|
||||||
@ -131,6 +137,11 @@ public class ListProjects {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ListProjects setMatchPrefix(String prefix) {
|
||||||
|
this.matchPrefix = prefix;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public void display(OutputStream out) {
|
public void display(OutputStream out) {
|
||||||
final PrintWriter stdout;
|
final PrintWriter stdout;
|
||||||
try {
|
try {
|
||||||
@ -140,13 +151,14 @@ public class ListProjects {
|
|||||||
throw new RuntimeException("JVM lacks UTF-8 encoding", e);
|
throw new RuntimeException("JVM lacks UTF-8 encoding", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int found = 0;
|
||||||
Map<String, ProjectInfo> output = Maps.newTreeMap();
|
Map<String, ProjectInfo> output = Maps.newTreeMap();
|
||||||
Map<String, String> hiddenNames = Maps.newHashMap();
|
Map<String, String> hiddenNames = Maps.newHashMap();
|
||||||
|
|
||||||
final TreeMap<Project.NameKey, ProjectNode> treeMap =
|
final TreeMap<Project.NameKey, ProjectNode> treeMap =
|
||||||
new TreeMap<Project.NameKey, ProjectNode>();
|
new TreeMap<Project.NameKey, ProjectNode>();
|
||||||
try {
|
try {
|
||||||
for (final Project.NameKey projectName : projectCache.all()) {
|
for (final Project.NameKey projectName : scan()) {
|
||||||
final ProjectState e = projectCache.get(projectName);
|
final ProjectState e = projectCache.get(projectName);
|
||||||
if (e == null) {
|
if (e == null) {
|
||||||
// If we can't get it from the cache, pretend its not present.
|
// If we can't get it from the cache, pretend its not present.
|
||||||
@ -233,6 +245,10 @@ public class ListProjects {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (limit > 0 && ++found > limit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (format.isJson()) {
|
if (format.isJson()) {
|
||||||
output.put(info.name, info);
|
output.put(info.name, info);
|
||||||
continue;
|
continue;
|
||||||
@ -270,6 +286,14 @@ public class ListProjects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Iterable<NameKey> scan() {
|
||||||
|
if (matchPrefix != null) {
|
||||||
|
return projectCache.byName(matchPrefix);
|
||||||
|
} else {
|
||||||
|
return projectCache.all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void printProjectTree(final PrintWriter stdout,
|
private void printProjectTree(final PrintWriter stdout,
|
||||||
final TreeMap<Project.NameKey, ProjectNode> treeMap) {
|
final TreeMap<Project.NameKey, ProjectNode> treeMap) {
|
||||||
final SortedSet<ProjectNode> sortedNodes = new TreeSet<ProjectNode>();
|
final SortedSet<ProjectNode> sortedNodes = new TreeSet<ProjectNode>();
|
||||||
|
Loading…
Reference in New Issue
Block a user