File D:\maven-qcr-plugin-testsunitaires\qcr4java\jenkins-master\target\site\qc\core\src\main\java\hudson\search\Search.java.html

1 /*
2 * The MIT License
3 *
4 * Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi,
5 * Yahoo!, Inc.
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
13 *
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 * THE SOFTWARE.
24 */
25 package hudson.search;
26
27 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
28
29 import hudson.Util;
30 import hudson.util.EditDistance;
31 import java.io.IOException;
32 import java.util.AbstractList;
33 import java.util.ArrayList;
34 import java.util.Collections;
35 import java.util.HashSet;
36 import java.util.List;
37 import java.util.Set;
38 import java.util.logging.Level;
39 import java.util.logging.Logger;
40
41 import javax.servlet.ServletException;
42 import org.kohsuke.stapler.Ancestor;
43 import org.kohsuke.stapler.QueryParameter;
44 import org.kohsuke.stapler.StaplerRequest;
45 import org.kohsuke.stapler.StaplerResponse;
46 import org.kohsuke.stapler.export.DataWriter;
47 import org.kohsuke.stapler.export.Exported;
48 import org.kohsuke.stapler.export.ExportedBean;
49 import org.kohsuke.stapler.export.Flavor;
50
51 /**
52 * Web-bound object that provides search/navigation capability.
53 *
54 * <p>
55 * This object is bound to "./search" of a model object via {@link SearchableModelObject} and serves
56 * HTTP requests coming from JavaScript to provide search result and auto-completion.
57 *
58 * @author Kohsuke Kawaguchi
59 * @see SearchableModelObject
60 */
61 public class Search {
62 public void doIndex(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
63 List<Ancestor> l = req.getAncestors();
64 for( int i=l.size()-1; i>=0; i-- ) {
65 Ancestor a = l.get(i);
66 if (a.getObject() instanceof SearchableModelObject) {
67 SearchableModelObject smo = (SearchableModelObject) a.getObject();
68 if(LOGGER.isLoggable(Level.FINE)){
69 LOGGER.fine(String.format("smo.displayName=%s, searchName=%s",smo.getDisplayName(), smo.getSearchName()));
70 }
71
72 SearchIndex index = smo.getSearchIndex();
73 String query = req.getParameter("q");
74 if(query!=null) {
75 SuggestedItem target = find(index, query);
76 if(target!=null) {
77 // found
78 rsp.sendRedirect2(a.getUrl()+target.getUrl());
79 return;
80 }
81 }
82 }
83 }
84
85 // no exact match. show the suggestions
86 rsp.setStatus(SC_NOT_FOUND);
87 req.getView(this,"search-failed.jelly").forward(req,rsp);
88 }
89
90 /**
91 * Used by OpenSearch auto-completion. Returns JSON array of the form:
92 *
93 * <pre>
94 * ["queryString",["comp1","comp2",...]]
95 * </pre>
96 *
97 * See http://developer.mozilla.org/en/docs/Supporting_search_suggestions_in_search_plugins
98 */
99 public void doSuggestOpenSearch(StaplerRequest req, StaplerResponse rsp, @QueryParameter String q) throws IOException, ServletException {
100 rsp.setContentType(Flavor.JSON.contentType);
101 DataWriter w = Flavor.JSON.createDataWriter(null, rsp);
102 w.startArray();
103 w.value(q);
104
105 w.startArray();
106 for (SuggestedItem item : getSuggestions(req, q))
107 w.value(item.getPath());
108 w.endArray();
109 w.endArray();
110 }
111
112 /**
113 * Used by search box auto-completion. Returns JSON array.
114 */
115 public void doSuggest(StaplerRequest req, StaplerResponse rsp, @QueryParameter String query) throws IOException, ServletException {
116 Result r = new Result();
117 for (SuggestedItem item : getSuggestions(req, query))
118 r.suggestions.add(new Item(item.getPath()));
119
120 rsp.serveExposedBean(req,r,Flavor.JSON);
121 }
122
123 /**
124 * Gets the list of suggestions that match the given query.
125 *
126 * @return
127 * can be empty but never null. The size of the list is always smaller than
128 * a certain threshold to avoid showing too many options.
129 */
130 public SearchResult getSuggestions(StaplerRequest req, String query) {
131 Set<String> paths = new HashSet<String>(); // paths already added, to control duplicates
132 SearchResultImpl r = new SearchResultImpl();
133 int max = req.hasParameter("max") ? Integer.parseInt(req.getParameter("max")) : 20;
134 for (SuggestedItem i : suggest(makeSuggestIndex(req), query)) {
135 if(r.size()>=max) {
136 r.hasMoreResults = true;
137 break;
138 }
139 if(paths.add(i.getPath()))
140 r.add(i);
141 }
142 return r;
143 }
144
145 /**
146 * Creates merged search index for suggestion.
147 */
148 private SearchIndex makeSuggestIndex(StaplerRequest req) {
149 SearchIndexBuilder builder = new SearchIndexBuilder();
150 for (Ancestor a : req.getAncestors()) {
151 if (a.getObject() instanceof SearchableModelObject) {
152 SearchableModelObject smo = (SearchableModelObject) a.getObject();
153 builder.add(smo.getSearchIndex());
154 }
155 }
156 return builder.make();
157 }
158
159 private static class SearchResultImpl extends ArrayList<SuggestedItem> implements SearchResult {
160
161 private boolean hasMoreResults = false;
162
163 public boolean hasMoreResults() {
164 return hasMoreResults;
165 }
166 }
167
168 @ExportedBean
169 public static class Result {
170 @Exported
171 public List<Item> suggestions = new ArrayList<Item>();
172 }
173
174 @ExportedBean(defaultVisibility=999)
175 public static class Item {
176 @Exported
177 public String name;
178
179 public Item(String name) {
180 this.name = name;
181 }
182 }
183
184 private enum Mode {
185 FIND {
186 void find(SearchIndex index, String token, List<SearchItem> result) {
187 index.find(token, result);
188 }
189 },
190 SUGGEST {
191 void find(SearchIndex index, String token, List<SearchItem> result) {
192 index.suggest(token, result);
193 }
194 };
195
196 abstract void find(SearchIndex index, String token, List<SearchItem> result);
197
198 }
199
200 /**
201 * When there are mutiple suggested items, this method can narrow down the resultset
202 * to the SuggestedItem that has a url that contains the query. This is useful is one
203 * job has a display name that matches another job's project name.
204 * @param r A list of Suggested items. It is assumed that there is at least one
205 * SuggestedItem in r.
206 * @param query A query string
207 * @return Returns the SuggestedItem which has a search url that contains the query.
208 * If no SuggestedItems have a search url which contains the query, then the first
209 * SuggestedItem in the List is returned.
210 */
211 static SuggestedItem findClosestSuggestedItem(List<SuggestedItem> r, String query) {
212 for(SuggestedItem curItem : r) {
213 if(LOGGER.isLoggable(Level.FINE)) {
214 LOGGER.fine(String.format("item's searchUrl:%s;query=%s", curItem.item.getSearchUrl(), query));
215 }
216 if(curItem.item.getSearchUrl().contains(Util.rawEncode(query))) {
217 return curItem;
218 }
219 }
220
221 // couldn't find an item with the query in the url so just
222 // return the first one
223 return r.get(0);
224 }
225
226 /**
227 * Performs a search and returns the match, or null if no match was found
228 * or more than one match was found
229 */
230 public static SuggestedItem find(SearchIndex index, String query) {
231 List<SuggestedItem> r = find(Mode.FIND, index, query);
232 if(r.isEmpty()){
233 return null;
234 }
235 else if(1==r.size()){
236 return r.get(0);
237 }
238 else {
239 // we have more than one suggested item, so return the item who's url
240 // contains the query as this is probably the job's name
241 return findClosestSuggestedItem(r, query);
242 }
243
244 }
245
246 public static List<SuggestedItem> suggest(SearchIndex index, final String tokenList) {
247
248 class Tag implements Comparable<Tag>{
249 final SuggestedItem item;
250 final int distance;
251 /** If the path to this suggestion starts with the token list, 1. Otherwise 0. */
252 final int prefixMatch;
253
254 Tag(SuggestedItem i) {
255 item = i;
256 distance = EditDistance.editDistance(i.getPath(),tokenList);
257 prefixMatch = i.getPath().startsWith(tokenList)?1:0;
258 }
259
260 public int compareTo(Tag that) {
261 int r = this.prefixMatch -that.prefixMatch;
262 if(r!=0) return -r; // ones with head match should show up earlier
263 return this.distance-that.distance;
264 }
265 }
266
267 List<Tag> buf = new ArrayList<Tag>();
268 List<SuggestedItem> items = find(Mode.SUGGEST, index, tokenList);
269
270 // sort them
271 for( SuggestedItem i : items)
272 buf.add(new Tag(i));
273 Collections.sort(buf);
274 items.clear();
275 for (Tag t : buf)
276 items.add(t.item);
277
278 return items;
279 }
280
281 static final class TokenList {
282 private final String[] tokens;
283
284 private final static String[] EMPTY = new String[0];
285
286 public TokenList(String tokenList) {
287 tokens = tokenList!=null ? tokenList.split("(?<=\\s)(?=\\S)") : EMPTY;
288 }
289
290 public int length() { return tokens.length; }
291
292 /**
293 * Returns {@link List} such that its <tt>get(end)</tt>
294 * returns the concatanation of [token_start,...,token_end]
295 * (both end inclusive.)
296 */
297 public List<String> subSequence(final int start) {
298 return new AbstractList<String>() {
299 public String get(int index) {
300 StringBuilder buf = new StringBuilder();
301 for(int i=start; i<=start+index; i++ )
302 buf.append(tokens[i]);
303 return buf.toString().trim();
304 }
305
306 public int size() {
307 return tokens.length-start;
308 }
309 };
310 }
311
312
313 public String toString() {
314 StringBuilder s = new StringBuilder("TokenList{");
315 for(String token : tokens) {
316 s.append(token);
317 s.append(",");
318 }
319 s.append('}');
320
321 return s.toString();
322 }
323 }
324
325 private static List<SuggestedItem> find(Mode m, SearchIndex index, String tokenList) {
326 TokenList tokens = new TokenList(tokenList);
327 if(tokens.length()==0) return Collections.emptyList(); // no tokens given
328
329 List<SuggestedItem>[] paths = new List[tokens.length()+1]; // we won't use [0].
330 for(int i=1;i<=tokens.length();i++)
331 paths[i] = new ArrayList<SuggestedItem>();
332
333 List<SearchItem> items = new ArrayList<SearchItem>(); // items found in 1 step
334
335 if(LOGGER.isLoggable(Level.FINE)) {
336 LOGGER.fine("tokens="+tokens.toString());
337 }
338
339 // first token
340 int w=1; // width of token
341 for (String token : tokens.subSequence(0)) {
342 items.clear();
343 m.find(index,token,items);
344 for (SearchItem si : items) {
345 paths[w].add(new SuggestedItem(si));
346 LOGGER.info("found search item:" + si.getSearchName());
347 }
348 w++;
349 }
350
351 // successive tokens
352 for (int j=1; j<tokens.length(); j++) {
353 // for each length
354 w=1;
355 for (String token : tokens.subSequence(j)) {
356 // for each candidate
357 for (SuggestedItem r : paths[j]) {
358 items.clear();
359 m.find(r.item.getSearchIndex(),token,items);
360 for (SearchItem i : items)
361 paths[j+w].add(new SuggestedItem(r,i));
362 }
363 w++;
364 }
365 }
366
367 return paths[tokens.length()];
368 }
369
370 private final static Logger LOGGER = Logger.getLogger(Search.class.getName());
371 }
372