/hudson-core/src/main/java/hudson/search/Search.java

http://github.com/hudson/hudson · Java · 292 lines · 187 code · 38 blank · 67 comment · 25 complexity · 1fe2e37ad79156a7add56cf8261f1a63 MD5 · raw file

  1. /*
  2. * The MIT License
  3. *
  4. * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
  5. *
  6. * Permission is hereby granted, free of charge, to any person obtaining a copy
  7. * of this software and associated documentation files (the "Software"), to deal
  8. * in the Software without restriction, including without limitation the rights
  9. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. * copies of the Software, and to permit persons to whom the Software is
  11. * furnished to do so, subject to the following conditions:
  12. *
  13. * The above copyright notice and this permission notice shall be included in
  14. * all copies or substantial portions of the Software.
  15. *
  16. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. * THE SOFTWARE.
  23. */
  24. package hudson.search;
  25. import hudson.util.EditDistance;
  26. import org.apache.commons.lang3.StringUtils;
  27. import org.kohsuke.stapler.Ancestor;
  28. import org.kohsuke.stapler.QueryParameter;
  29. import org.kohsuke.stapler.StaplerRequest;
  30. import org.kohsuke.stapler.StaplerResponse;
  31. import org.kohsuke.stapler.export.DataWriter;
  32. import org.kohsuke.stapler.export.Exported;
  33. import org.kohsuke.stapler.export.ExportedBean;
  34. import org.kohsuke.stapler.export.Flavor;
  35. import javax.servlet.ServletException;
  36. import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
  37. import java.io.IOException;
  38. import java.util.AbstractList;
  39. import java.util.ArrayList;
  40. import java.util.Collections;
  41. import java.util.HashSet;
  42. import java.util.List;
  43. import java.util.Set;
  44. /**
  45. * Web-bound object that serves QuickSilver-like search requests.
  46. *
  47. * @author Kohsuke Kawaguchi
  48. */
  49. public class Search {
  50. public void doIndex(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
  51. List<Ancestor> l = req.getAncestors();
  52. for( int i=l.size()-1; i>=0; i-- ) {
  53. Ancestor a = l.get(i);
  54. if (a.getObject() instanceof SearchableModelObject) {
  55. SearchableModelObject smo = (SearchableModelObject) a.getObject();
  56. SearchIndex index = smo.getSearchIndex();
  57. String query = req.getParameter("q");
  58. if(!StringUtils.isEmpty(query)) {
  59. SuggestedItem target = find(index, query);
  60. if(target!=null) {
  61. // found
  62. rsp.sendRedirect2(a.getUrl()+target.getUrl());
  63. return;
  64. }
  65. }
  66. }
  67. }
  68. // no exact match. show the suggestions
  69. rsp.setStatus(SC_NOT_FOUND);
  70. req.getView(this,"search-failed.jelly").forward(req,rsp);
  71. }
  72. /**
  73. * Used by OpenSearch auto-completion. Returns JSON array of the form:
  74. *
  75. * <pre>
  76. * ["queryString",["comp1","comp2",...]]
  77. * </pre>
  78. *
  79. * See http://developer.mozilla.org/en/docs/Supporting_search_suggestions_in_search_plugins
  80. */
  81. public void doSuggestOpenSearch(StaplerRequest req, StaplerResponse rsp, @QueryParameter String q) throws IOException, ServletException {
  82. DataWriter w = Flavor.JSON.createDataWriter(null, rsp);
  83. w.startArray();
  84. w.value(q);
  85. w.startArray();
  86. for (SuggestedItem item : getSuggestions(req, q))
  87. w.value(item.getPath());
  88. w.endArray();
  89. w.endArray();
  90. }
  91. /**
  92. * Used by search box auto-completion. Returns JSON array.
  93. */
  94. public void doSuggest(StaplerRequest req, StaplerResponse rsp, @QueryParameter String query) throws IOException, ServletException {
  95. Result r = new Result();
  96. for (SuggestedItem item : getSuggestions(req, query))
  97. r.suggestions.add(new Item(item.getPath()));
  98. rsp.serveExposedBean(req,r,Flavor.JSON);
  99. }
  100. /**
  101. * Gets the list of suggestions that match the given query.
  102. *
  103. * @return
  104. * can be empty but never null. The size of the list is always smaller than
  105. * a certain threshold to avoid showing too many options.
  106. */
  107. public List<SuggestedItem> getSuggestions(StaplerRequest req, String query) {
  108. Set<String> paths = new HashSet<String>(); // paths already added, to control duplicates
  109. List<SuggestedItem> r = new ArrayList<SuggestedItem>();
  110. for (SuggestedItem i : suggest(makeSuggestIndex(req), query)) {
  111. if(r.size()>20) break;
  112. if(paths.add(i.getPath()))
  113. r.add(i);
  114. }
  115. return r;
  116. }
  117. /**
  118. * Creates merged search index for suggestion.
  119. */
  120. private SearchIndex makeSuggestIndex(StaplerRequest req) {
  121. SearchIndexBuilder builder = new SearchIndexBuilder();
  122. for (Ancestor a : req.getAncestors()) {
  123. if (a.getObject() instanceof SearchableModelObject) {
  124. SearchableModelObject smo = (SearchableModelObject) a.getObject();
  125. builder.add(smo.getSearchIndex());
  126. }
  127. }
  128. return builder.make();
  129. }
  130. @ExportedBean
  131. public static class Result {
  132. //TODO: review and check whether we can do it private
  133. @Exported
  134. public List<Item> suggestions = new ArrayList<Item>();
  135. public List<Item> getSuggestions() {
  136. return suggestions;
  137. }
  138. }
  139. @ExportedBean(defaultVisibility=999)
  140. public static class Item {
  141. @Exported
  142. public String name;
  143. public Item(String name) {
  144. this.name = name;
  145. }
  146. }
  147. private enum Mode {
  148. FIND {
  149. void find(SearchIndex index, String token, List<SearchItem> result) {
  150. index.find(token, result);
  151. }
  152. },
  153. SUGGEST {
  154. void find(SearchIndex index, String token, List<SearchItem> result) {
  155. index.suggest(token, result);
  156. }
  157. };
  158. abstract void find(SearchIndex index, String token, List<SearchItem> result);
  159. }
  160. /**
  161. * Performs a search and returns the match, or null if no match was found.
  162. */
  163. public static SuggestedItem find(SearchIndex index, String query) {
  164. List<SuggestedItem> r = find(Mode.FIND, index, query);
  165. if(r.isEmpty()) return null;
  166. else return r.get(0);
  167. }
  168. public static List<SuggestedItem> suggest(SearchIndex index, final String tokenList) {
  169. class Tag implements Comparable<Tag>{
  170. final SuggestedItem item;
  171. final int distance;
  172. /** If the path to this suggestion starts with the token list, 1. Otherwise 0. */
  173. final int prefixMatch;
  174. Tag(SuggestedItem i) {
  175. item = i;
  176. distance = EditDistance.editDistance(i.getPath(),tokenList);
  177. prefixMatch = i.getPath().startsWith(tokenList)?1:0;
  178. }
  179. public int compareTo(Tag that) {
  180. int r = this.prefixMatch -that.prefixMatch;
  181. if(r!=0) return -r; // ones with head match should show up earlier
  182. return this.distance-that.distance;
  183. }
  184. }
  185. List<Tag> buf = new ArrayList<Tag>();
  186. List<SuggestedItem> items = find(Mode.SUGGEST, index, tokenList);
  187. // sort them
  188. for( SuggestedItem i : items)
  189. buf.add(new Tag(i));
  190. Collections.sort(buf);
  191. items.clear();
  192. for (Tag t : buf)
  193. items.add(t.item);
  194. return items;
  195. }
  196. static final class TokenList {
  197. private final String[] tokens;
  198. public TokenList(String tokenList) {
  199. tokens = tokenList.split("(?<=\\s)(?=\\S)");
  200. }
  201. public int length() { return tokens.length; }
  202. /**
  203. * Returns {@link List} such that its <tt>get(end)</tt>
  204. * returns the concatanation of [token_start,...,token_end]
  205. * (both end inclusive.)
  206. */
  207. public List<String> subSequence(final int start) {
  208. return new AbstractList<String>() {
  209. public String get(int index) {
  210. StringBuilder buf = new StringBuilder();
  211. for(int i=start; i<=start+index; i++ )
  212. buf.append(tokens[i]);
  213. return buf.toString().trim();
  214. }
  215. public int size() {
  216. return tokens.length-start;
  217. }
  218. };
  219. }
  220. }
  221. private static List<SuggestedItem> find(Mode m, SearchIndex index, String tokenList) {
  222. TokenList tokens = new TokenList(tokenList);
  223. if(tokens.length()==0) return Collections.emptyList(); // no tokens given
  224. List<SuggestedItem>[] paths = new List[tokens.length()+1]; // we won't use [0].
  225. for(int i=1;i<=tokens.length();i++)
  226. paths[i] = new ArrayList<SuggestedItem>();
  227. List<SearchItem> items = new ArrayList<SearchItem>(); // items found in 1 step
  228. // first token
  229. int w=1; // width of token
  230. for (String token : tokens.subSequence(0)) {
  231. items.clear();
  232. m.find(index,token,items);
  233. for (SearchItem si : items)
  234. paths[w].add(new SuggestedItem(si));
  235. w++;
  236. }
  237. // successive tokens
  238. for (int j=1; j<tokens.length(); j++) {
  239. // for each length
  240. w=1;
  241. for (String token : tokens.subSequence(j)) {
  242. // for each candidate
  243. for (SuggestedItem r : paths[j]) {
  244. items.clear();
  245. m.find(r.item.getSearchIndex(),token,items);
  246. for (SearchItem i : items)
  247. paths[j+w].add(new SuggestedItem(r,i));
  248. }
  249. w++;
  250. }
  251. }
  252. return paths[tokens.length()];
  253. }
  254. }