REST endpoint for searching Gerrit documentation.

The documentation of this endpoint is available in
rest-api-docsearch[.txt|.html].

The UI of showing the search result and doing the search will be in a
separate change.

Change-Id: Ifa4f5a7d576ada7f88a4fa1b765a38cba6d7e964
This commit is contained in:
Yuxuan 'fishy' Wang
2013-10-14 12:39:30 -07:00
parent 6e4a19e9df
commit ec4b06a6e2
14 changed files with 436 additions and 18 deletions

View File

@@ -1,11 +1,24 @@
SRCS = glob(['src/main/java/**/*.java'])
CONSTANTS_SRC = [
'src/main/java/com/google/gerrit/httpd/rpc/doc/Constants.java',
]
SRCS = glob(
['src/main/java/**/*.java'],
excludes = CONSTANTS_SRC,
)
RESOURCES = glob(['src/main/resources/**/*'])
java_library2(
name = 'constants',
srcs = CONSTANTS_SRC,
visibility = ['PUBLIC'],
)
java_library2(
name = 'httpd',
srcs = SRCS,
resources = RESOURCES,
deps = [
':constants',
'//gerrit-antlr:query_exception',
'//gerrit-common:server',
'//gerrit-extension-api:api',
@@ -30,6 +43,9 @@ java_library2(
'//lib/jgit:jgit',
'//lib/jgit:jgit-servlet',
'//lib/log:api',
'//lib/lucene:analyzers-common',
'//lib/lucene:core',
'//lib/lucene:query-parser',
],
compile_deps = ['//lib:servlet-api-3_0'],
visibility = ['PUBLIC'],

View File

@@ -30,6 +30,7 @@ import com.google.gerrit.httpd.rpc.account.AccountsRestApiServlet;
import com.google.gerrit.httpd.rpc.change.ChangesRestApiServlet;
import com.google.gerrit.httpd.rpc.change.DeprecatedChangeQueryServlet;
import com.google.gerrit.httpd.rpc.config.ConfigRestApiServlet;
import com.google.gerrit.httpd.rpc.doc.QueryDocumentationFilter;
import com.google.gerrit.httpd.rpc.group.GroupsRestApiServlet;
import com.google.gerrit.httpd.rpc.project.ProjectsRestApiServlet;
import com.google.gerrit.reviewdb.client.Change;
@@ -110,6 +111,8 @@ class UrlModule extends ServletModule {
serveRegex("^/(?:a/)?groups/(.*)?$").with(GroupsRestApiServlet.class);
serveRegex("^/(?:a/)?projects/(.*)?$").with(ProjectsRestApiServlet.class);
filter("/Documentation/").through(QueryDocumentationFilter.class);
if (cfg.deprecatedQuery) {
serve("/query").with(DeprecatedChangeQueryServlet.class);
}

View File

@@ -607,7 +607,7 @@ public class RestApiServlet extends HttpServlet {
throw new InstantiationException("Cannot make " + type);
}
private static void replyJson(@Nullable HttpServletRequest req,
public static void replyJson(@Nullable HttpServletRequest req,
HttpServletResponse res,
Multimap<String, String> config,
Object result)

View File

@@ -0,0 +1,24 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.httpd.rpc.doc;
public class Constants {
public static final String DOC_FIELD = "doc";
public static final String TITLE_FIELD = "title";
public static final String URL_FIELD = "url";
private Constants() {}
}

View File

@@ -0,0 +1,191 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.httpd.rpc.doc;
import com.google.common.base.Strings;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.gerrit.httpd.restapi.RestApiServlet;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IndexOutput;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.util.Version;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Singleton
public class QueryDocumentationFilter implements Filter {
private static final Logger log =
LoggerFactory.getLogger(QueryDocumentationFilter.class);
private static final String INDEX_PATH = "index.zip";
private static final Version LUCENE_VERSION = Version.LUCENE_44;
private IndexSearcher searcher;
private QueryParser parser;
protected static class DocResult {
public String title;
public String url;
public String content;
}
@Inject
QueryDocumentationFilter() {
}
@Override
public void init(FilterConfig filterConfig) {
try {
Directory dir = readIndexDirectory();
if (dir == null) {
searcher = null;
parser = null;
return;
}
IndexReader reader = DirectoryReader.open(dir);
searcher = new IndexSearcher(reader);
StandardAnalyzer analyzer = new StandardAnalyzer(LUCENE_VERSION);
parser = new QueryParser(LUCENE_VERSION, Constants.DOC_FIELD, analyzer);
} catch (IOException e) {
log.error("Cannot initialize documentation full text index", e);
searcher = null;
parser = null;
}
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
if ("GET".equals(req.getMethod())
&& !Strings.isNullOrEmpty(req.getParameter("q"))) {
HttpServletResponse rsp = (HttpServletResponse) response;
try {
List<DocResult> result = doQuery(request.getParameter("q"));
Multimap<String, String> config = LinkedHashMultimap.create();
RestApiServlet.replyJson(req, rsp, config, result);
} catch (DocQueryException e) {
rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
} else {
chain.doFilter(request, response);
}
}
private List<DocResult> doQuery(String q) throws DocQueryException {
if (parser == null || searcher == null) {
throw new DocQueryException("Not initialized");
}
try {
Query query = parser.parse(q);
TopDocs results = searcher.search(query, Integer.MAX_VALUE);
ScoreDoc[] hits = results.scoreDocs;
int totalHits = results.totalHits;
List<DocResult> out = Lists.newArrayListWithCapacity(totalHits);
for (int i = 0; i < totalHits; i++) {
DocResult result = new DocResult();
Document doc = searcher.doc(hits[i].doc);
result.url = doc.get(Constants.URL_FIELD);
result.title = doc.get(Constants.TITLE_FIELD);
out.add(result);
}
return out;
} catch (IOException e) {
throw new DocQueryException(e);
} catch (ParseException e) {
throw new DocQueryException(e);
}
}
protected Directory readIndexDirectory() throws IOException {
Directory dir = new RAMDirectory();
byte[] buffer = new byte[4096];
InputStream index =
QueryDocumentationFilter.class.getClassLoader().getResourceAsStream(INDEX_PATH);
if (index == null) {
log.warn("No index available");
return null;
}
ZipInputStream zip = new ZipInputStream(index);
try {
ZipEntry entry;
while ((entry = zip.getNextEntry()) != null) {
IndexOutput out = dir.createOutput(entry.getName(), null);
int count;
while ((count = zip.read(buffer)) != -1) {
out.writeBytes(buffer, count);
}
out.close();
}
} finally {
zip.close();
}
// We must NOT call dir.close() here, as DirectoryReader.open() expects an opened directory.
return dir;
}
private static class DocQueryException extends Exception {
public DocQueryException() {
}
public DocQueryException(String msg) {
super(msg);
}
public DocQueryException(String msg, Throwable e) {
super(msg, e);
}
public DocQueryException(Throwable e) {
super(e);
}
}
}