* Search the specified repositories using the Lucene query.\r
* \r
* @param query\r
- * @param maximumHits\r
+ * @param page\r
+ * @param pageSize\r
* @param repositories\r
* @return\r
*/\r
- public List<SearchResult> search(String query, int maximumHits, List<String> repositories) {\r
- List<SearchResult> srs = luceneExecutor.search(query, maximumHits, repositories);\r
+ public List<SearchResult> search(String query, int page, int pageSize, List<String> repositories) { \r
+ List<SearchResult> srs = luceneExecutor.search(query, page, pageSize, repositories);\r
return srs;\r
}\r
\r
return false;\r
}\r
\r
- private SearchResult createSearchResult(Document doc, float score) throws ParseException {\r
+ private SearchResult createSearchResult(Document doc, float score, int hitId, int totalHits) throws ParseException {\r
SearchResult result = new SearchResult();\r
+ result.hitId = hitId;\r
+ result.totalHits = totalHits;\r
result.score = score;\r
result.date = DateTools.stringToDate(doc.get(FIELD_DATE));\r
result.summary = doc.get(FIELD_SUMMARY); \r
* \r
* @param text\r
* if the text is null or empty, null is returned\r
- * @param maximumHits\r
- * the maximum number of hits to collect\r
+ * @param page\r
+ * the page number to retrieve. page is 1-indexed.\r
+ * @param pageSize\r
+ * the number of elements to return for this page\r
* @param repositories\r
* a list of repositories to search. if no repositories are\r
* specified null is returned.\r
* @return a list of SearchResults in order from highest to the lowest score\r
* \r
*/\r
- public List<SearchResult> search(String text, int maximumHits, List<String> repositories) {\r
+ public List<SearchResult> search(String text, int page, int pageSize, List<String> repositories) {\r
if (ArrayUtils.isEmpty(repositories)) {\r
return null;\r
}\r
- return search(text, maximumHits, repositories.toArray(new String[0]));\r
+ return search(text, page, pageSize, repositories.toArray(new String[0]));\r
}\r
\r
/**\r
* \r
* @param text\r
* if the text is null or empty, null is returned\r
- * @param maximumHits\r
- * the maximum number of hits to collect\r
+ * @param page\r
+ * the page number to retrieve. page is 1-indexed.\r
+ * @param pageSize\r
+ * the number of elements to return for this page\r
* @param repositories\r
* a list of repositories to search. if no repositories are\r
* specified null is returned.\r
* @return a list of SearchResults in order from highest to the lowest score\r
* \r
- */ \r
- public List<SearchResult> search(String text, int maximumHits, String... repositories) {\r
+ */\r
+ public List<SearchResult> search(String text, int page, int pageSize, String... repositories) {\r
if (StringUtils.isEmpty(text)) {\r
return null;\r
}\r
searcher = new IndexSearcher(reader);\r
}\r
Query rewrittenQuery = searcher.rewrite(query);\r
- TopScoreDocCollector collector = TopScoreDocCollector.create(maximumHits, true);\r
+ TopScoreDocCollector collector = TopScoreDocCollector.create(5000, true);\r
searcher.search(rewrittenQuery, collector);\r
- ScoreDoc[] hits = collector.topDocs().scoreDocs;\r
+ int offset = Math.max(0, (page - 1) * pageSize);\r
+ ScoreDoc[] hits = collector.topDocs(offset, pageSize).scoreDocs;\r
+ int totalHits = collector.getTotalHits();\r
for (int i = 0; i < hits.length; i++) {\r
int docId = hits[i].doc;\r
Document doc = searcher.doc(docId);\r
- // TODO identify the source index for the doc, then eliminate FIELD_REPOSITORY \r
- SearchResult result = createSearchResult(doc, hits[i].score);\r
+ SearchResult result = createSearchResult(doc, hits[i].score, offset + i + 1, totalHits);\r
if (repositories.length == 1) {\r
// single repository search\r
result.repository = repositories[0];\r
public class SearchResult implements Serializable {\r
\r
private static final long serialVersionUID = 1L;\r
+ \r
+ public int hitId;\r
+ \r
+ public int totalHits;\r
\r
public float score;\r
\r
</div>\r
</div>\r
</form>\r
- <hr/>\r
- <div class="row-fluid">\r
+\r
+ <div class="row-fluid"> \r
+ <!-- results header -->\r
+ <div class="span8">\r
+ <h3><span wicket:id="resultsHeader"></span> <small><span wicket:id="resultsCount"></span></small></h3>\r
+ </div>\r
+ <!-- pager links -->\r
+ <div class="span4" wicket:id="topPager"></div>\r
+ </div>\r
+ \r
+ <div class="row-fluid"> \r
+ <!-- search result repeater -->\r
<div class="searchResult" wicket:id="searchResults">\r
<div><i wicket:id="type"></i><span class="summary" wicket:id="summary"></span></div>\r
<div class="body">\r
<span class="repository" wicket:id="repository"></span>:<span class="branch" wicket:id="branch"></span> \r
</div>\r
</div>\r
- </div>\r
+\r
+ <!-- pager links -->\r
+ <div wicket:id="bottomPager"></div>\r
+\r
+ </div> \r
</body>\r
</wicket:extend>\r
</html>
\ No newline at end of file
*/\r
package com.gitblit.wicket.pages;\r
\r
+import java.text.MessageFormat;\r
import java.util.ArrayList;\r
import java.util.List;\r
\r
\r
import com.gitblit.Constants.SearchType;\r
import com.gitblit.GitBlit;\r
+import com.gitblit.Keys;\r
import com.gitblit.models.RepositoryModel;\r
import com.gitblit.models.SearchResult;\r
import com.gitblit.models.UserModel;\r
import com.gitblit.wicket.StringChoiceRenderer;\r
import com.gitblit.wicket.WicketUtils;\r
import com.gitblit.wicket.panels.LinkPanel;\r
+import com.gitblit.wicket.panels.PagerPanel;\r
\r
public class LucenePage extends RootPage {\r
\r
// default values\r
ArrayList<String> repositories = new ArrayList<String>(); \r
String query = "";\r
+ int page = 1;\r
+ int pageSize = GitBlit.getInteger(Keys.web.itemsPerPage, 50);\r
\r
if (params != null) {\r
String repository = WicketUtils.getRepositoryName(params);\r
if (!StringUtils.isEmpty(repository)) {\r
repositories.add(repository);\r
}\r
+\r
+ page = WicketUtils.getPage(params); \r
\r
if (params.containsKey("repositories")) {\r
String value = params.getString("repositories", "");\r
// execute search\r
final List<SearchResult> results = new ArrayList<SearchResult>();\r
if (!ArrayUtils.isEmpty(searchRepositories) && !StringUtils.isEmpty(query)) {\r
- results.addAll(GitBlit.self().search(query, 100, searchRepositories));\r
+ results.addAll(GitBlit.self().search(query, page, pageSize, searchRepositories));\r
+ }\r
+ \r
+ // results header\r
+ if (results.size() == 0) {\r
+ add(new Label("resultsHeader").setVisible(false));\r
+ add(new Label("resultsCount").setVisible(false));\r
+ } else {\r
+ add(new Label("resultsHeader", query).setRenderBodyOnly(true));\r
+ add(new Label("resultsCount", MessageFormat.format("results {0} - {1} ({2} hits)",\r
+ results.get(0).hitId, results.get(results.size() - 1).hitId, results.get(0).totalHits)).\r
+ setRenderBodyOnly(true));\r
}\r
\r
// search results view\r
}\r
item.add(new Label("fragment", sr.fragment).setEscapeModelStrings(false).setVisible(!StringUtils.isEmpty(sr.fragment)));\r
item.add(new LinkPanel("repository", null, sr.repository, SummaryPage.class, WicketUtils.newRepositoryParameter(sr.repository)));\r
- item.add(new LinkPanel("branch", "branch", StringUtils.getRelativePath(Constants.R_HEADS, sr.branch), LogPage.class, WicketUtils.newObjectParameter(sr.repository, sr.branch)));\r
+ if (StringUtils.isEmpty(sr.branch)) {\r
+ item.add(new Label("branch", "null"));\r
+ } else {\r
+ item.add(new LinkPanel("branch", "branch", StringUtils.getRelativePath(Constants.R_HEADS, sr.branch), LogPage.class, WicketUtils.newObjectParameter(sr.repository, sr.branch)));\r
+ }\r
item.add(new Label("author", sr.author));\r
item.add(WicketUtils.createDatestampLabel("date", sr.date, getTimeZone()));\r
}\r
};\r
add(resultsView.setVisible(results.size() > 0));\r
- } \r
+ \r
+ PageParameters pagerParams = new PageParameters();\r
+ pagerParams.put("repositories", StringUtils.flattenStrings(repositoriesModel.getObject()));\r
+ pagerParams.put("query", queryModel.getObject());\r
+ \r
+ int totalPages = 0;\r
+ if (results.size() > 0) {\r
+ totalPages = (results.get(0).totalHits / pageSize) + (results.get(0).totalHits % pageSize > 0 ? 1 : 0);\r
+ }\r
+ \r
+ add(new PagerPanel("topPager", page, totalPages, LucenePage.class, pagerParams));\r
+ add(new PagerPanel("bottomPager", page, totalPages, LucenePage.class, pagerParams));\r
+ }\r
+ \r
+// private String buildPager(int currentPage, int count, int total) {\r
+// int pages = (total / count) + (total % count == 0 ? 0 : 1);\r
+// \r
+// // pages are 1-indexed\r
+// // previous page link\r
+// if (currentPage <= 1) {\r
+// sb.append(MessageFormat.format(li, "disabled", "#", "←"));\r
+// } else {\r
+// List<String> parameters = new ArrayList<String>();\r
+// if (!StringUtils.isEmpty(penString)) {\r
+// parameters.add(penString);\r
+// }\r
+// parameters.add(MessageFormat.format(pg, currentPage - 1));\r
+// sb.append(MessageFormat.format(li, "", StringUtils.flattenStrings(parameters, "&"), "←"));\r
+// }\r
+//\r
+// // page links in middle\r
+// int minpage = Math.max(1, currentPage - Math.min(2, 2));\r
+// int maxpage = Math.min(pages, minpage + 4);\r
+// for (int i = minpage; i <= maxpage; i++) {\r
+// String cssClass = "";\r
+// if (i == currentPage) {\r
+// cssClass = "active";\r
+// }\r
+// List<String> parameters = new ArrayList<String>();\r
+// if (!StringUtils.isEmpty(penString)) {\r
+// parameters.add(penString);\r
+// }\r
+// parameters.add(MessageFormat.format(pg, i));\r
+// sb.append(MessageFormat.format(li, cssClass, StringUtils.flattenStrings(parameters, "&"), i));\r
+// }\r
+//\r
+// // next page link\r
+// if (currentPage == pages) {\r
+// sb.append(MessageFormat.format(li, "disabled", "#", "→"));\r
+// } else {\r
+// List<String> parameters = new ArrayList<String>();\r
+// if (!StringUtils.isEmpty(penString)) {\r
+// parameters.add(penString);\r
+// }\r
+// parameters.add(MessageFormat.format(pg, currentPage + 1));\r
+// sb.append(MessageFormat.format(li, "", StringUtils.flattenStrings(parameters, "&"), "→"));\r
+// }\r
+// return sb.toString();\r
+// }\r
+\r
}\r
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\r
+<html xmlns="http://www.w3.org/1999/xhtml" \r
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd" \r
+ xml:lang="en" \r
+ lang="en">\r
+<wicket:panel>\r
+ <div class="pagination pagination-right" style="margin: 0px;">\r
+ <ul>\r
+ <li wicket:id="page"><span wicket:id="pageLink"></span></li>\r
+ </ul> \r
+ </div>\r
+</wicket:panel>\r
+</html>
\ No newline at end of file
--- /dev/null
+/*\r
+ * Copyright 2012 gitblit.com.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ * http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package com.gitblit.wicket.panels;\r
+\r
+import java.io.Serializable;\r
+import java.util.ArrayList;\r
+import java.util.List;\r
+\r
+import org.apache.wicket.PageParameters;\r
+import org.apache.wicket.markup.html.panel.Panel;\r
+import org.apache.wicket.markup.repeater.Item;\r
+import org.apache.wicket.markup.repeater.data.DataView;\r
+import org.apache.wicket.markup.repeater.data.ListDataProvider;\r
+\r
+import com.gitblit.wicket.WicketUtils;\r
+import com.gitblit.wicket.pages.BasePage;\r
+\r
+public class PagerPanel extends Panel {\r
+\r
+ private static final long serialVersionUID = 1L;\r
+\r
+ public PagerPanel(String wicketId, final int currentPage, final int totalPages,\r
+ final Class<? extends BasePage> pageClass, final PageParameters baseParams) {\r
+ super(wicketId);\r
+ List<PageObject> pages = new ArrayList<PageObject>();\r
+ int[] deltas;\r
+ if (currentPage == 1) {\r
+ // [1], 2, 3, 4, 5\r
+ deltas = new int[] { 0, 1, 2, 3, 4 }; \r
+ } else if (currentPage == 2) {\r
+ // 1, [2], 3, 4, 5\r
+ deltas = new int[] { -1, 0, 1, 2, 3 }; \r
+ } else {\r
+ // 1, 2, [3], 4, 5\r
+ deltas = new int[] { -2, -1, 0, 1, 2 };\r
+ }\r
+\r
+ if (totalPages > 0) {\r
+ pages.add(new PageObject("\u2190", currentPage - 1));\r
+ }\r
+ for (int delta : deltas) {\r
+ int page = currentPage + delta;\r
+ if (page > 0 && page <= totalPages) {\r
+ pages.add(new PageObject("" + page, page));\r
+ }\r
+ }\r
+ if (totalPages > 0) {\r
+ pages.add(new PageObject("\u2192", currentPage + 1));\r
+ }\r
+\r
+ ListDataProvider<PageObject> pagesProvider = new ListDataProvider<PageObject>(pages);\r
+ final DataView<PageObject> pagesView = new DataView<PageObject>("page", pagesProvider) {\r
+ private static final long serialVersionUID = 1L;\r
+\r
+ public void populateItem(final Item<PageObject> item) {\r
+ PageObject pageItem = item.getModelObject();\r
+ PageParameters pageParams = new PageParameters(baseParams);\r
+ pageParams.put("pg", pageItem.page);\r
+ LinkPanel link = new LinkPanel("pageLink", null, pageItem.text, pageClass, pageParams);\r
+ link.setRenderBodyOnly(true);\r
+ item.add(link);\r
+ if (pageItem.page == currentPage || pageItem.page < 1 || pageItem.page > totalPages) {\r
+ WicketUtils.setCssClass(item, "disabled");\r
+ }\r
+ }\r
+ };\r
+ add(pagesView);\r
+ }\r
+\r
+ private class PageObject implements Serializable {\r
+\r
+ private static final long serialVersionUID = 1L;\r
+ \r
+ String text;\r
+ int page;\r
+\r
+ PageObject(String text, int page) {\r
+ this.text = text;\r
+ this.page = page;\r
+ }\r
+ }\r
+}\r
for (IssueModel anIssue : allIssues) {\r
lucene.index(name, anIssue);\r
}\r
- List<SearchResult> hits = lucene.search("working", 10, name);\r
+ List<SearchResult> hits = lucene.search("working", 1, 10, name);\r
assertTrue(hits.size() > 0);\r
\r
// reindex an issue\r
lucene.reindex(model, repository);\r
repository.close();\r
\r
- SearchResult result = lucene.search("type:blob AND path:bit.bit", 1, model.name).get(0); \r
+ SearchResult result = lucene.search("type:blob AND path:bit.bit", 1, 1, model.name).get(0); \r
assertEquals("Mike Donaghy", result.author);\r
- result = lucene.search("type:blob AND path:clipper.prg", 1, model.name).get(0); \r
+ result = lucene.search("type:blob AND path:clipper.prg", 1, 1, model.name).get(0); \r
assertEquals("tinogomes", result.author); \r
\r
// reindex theoretical physics\r
RepositoryModel model = newRepositoryModel(repository);\r
repository.close();\r
\r
- List<SearchResult> results = lucene.search("ada", 10, model.name);\r
+ List<SearchResult> results = lucene.search("ada", 1, 10, model.name);\r
assertEquals(2, results.size());\r
for (SearchResult res : results) {\r
assertEquals("refs/heads/master", res.branch);\r
}\r
\r
// author test\r
- results = lucene.search("author: tinogomes AND type:commit", 10, model.name);\r
+ results = lucene.search("author: tinogomes AND type:commit", 1, 10, model.name);\r
assertEquals(2, results.size());\r
\r
// blob test\r
- results = lucene.search("type: blob AND \"import std.stdio\"", 10, model.name);\r
+ results = lucene.search("type: blob AND \"import std.stdio\"", 1, 10, model.name);\r
assertEquals(1, results.size());\r
assertEquals("d.D", results.get(0).path);\r
\r
model = newRepositoryModel(repository);\r
repository.close();\r
\r
- results = lucene.search("\"add the .nojekyll file\"", 10, model.name);\r
+ results = lucene.search("\"add the .nojekyll file\"", 1, 10, model.name);\r
assertEquals(1, results.size());\r
assertEquals("Ondrej Certik", results.get(0).author);\r
assertEquals("2648c0c98f2101180715b4d432fc58d0e21a51d7", results.get(0).commitId);\r
assertEquals("refs/heads/gh-pages", results.get(0).branch);\r
\r
- results = lucene.search("type:blob AND \"src/intro.rst\"", 10, model.name);\r
+ results = lucene.search("type:blob AND \"src/intro.rst\"", 1, 10, model.name);\r
assertEquals(4, results.size());\r
\r
// hash id tests\r
- results = lucene.search("commit:57c4f26f157ece24b02f4f10f5f68db1d2ce7ff5", 10, model.name);\r
+ results = lucene.search("commit:57c4f26f157ece24b02f4f10f5f68db1d2ce7ff5", 1, 10, model.name);\r
assertEquals(1, results.size());\r
\r
- results = lucene.search("commit:57c4f26f157*", 10, model.name);\r
+ results = lucene.search("commit:57c4f26f157*", 1, 10, model.name);\r
assertEquals(1, results.size()); \r
\r
// annotated tag test\r
model = newRepositoryModel(repository);\r
repository.close();\r
\r
- results = lucene.search("I663208919f297836a9c16bf458e4a43ffaca4c12", 10, model.name);\r
+ results = lucene.search("I663208919f297836a9c16bf458e4a43ffaca4c12", 1, 10, model.name);\r
assertEquals(1, results.size());\r
assertEquals("[v1.3.0.201202151440-r]", results.get(0).tags.toString()); \r
\r
list.add(newRepositoryModel(repository).name);\r
repository.close();\r
\r
- List<SearchResult> results = lucene.search("test", 10, list);\r
+ List<SearchResult> results = lucene.search("test", 1, 10, list);\r
lucene.close();\r
assertEquals(10, results.size());\r
}\r