PageRenderTime 22ms CodeModel.GetById 3ms app.highlight 14ms RepoModel.GetById 1ms app.codeStats 0ms

/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 */
 24package hudson.search;
 25
 26import hudson.util.EditDistance;
 27import org.apache.commons.lang3.StringUtils;
 28import org.kohsuke.stapler.Ancestor;
 29import org.kohsuke.stapler.QueryParameter;
 30import org.kohsuke.stapler.StaplerRequest;
 31import org.kohsuke.stapler.StaplerResponse;
 32import org.kohsuke.stapler.export.DataWriter;
 33import org.kohsuke.stapler.export.Exported;
 34import org.kohsuke.stapler.export.ExportedBean;
 35import org.kohsuke.stapler.export.Flavor;
 36
 37import javax.servlet.ServletException;
 38import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
 39import java.io.IOException;
 40import java.util.AbstractList;
 41import java.util.ArrayList;
 42import java.util.Collections;
 43import java.util.HashSet;
 44import java.util.List;
 45import java.util.Set;
 46
 47/**
 48 * Web-bound object that serves QuickSilver-like search requests.
 49 *
 50 * @author Kohsuke Kawaguchi
 51 */
 52public class Search {
 53    public void doIndex(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
 54        List<Ancestor> l = req.getAncestors();
 55        for( int i=l.size()-1; i>=0; i-- ) {
 56            Ancestor a = l.get(i);
 57            if (a.getObject() instanceof SearchableModelObject) {
 58                SearchableModelObject smo = (SearchableModelObject) a.getObject();
 59
 60                SearchIndex index = smo.getSearchIndex();
 61                String query = req.getParameter("q");
 62                if(!StringUtils.isEmpty(query)) {
 63                    SuggestedItem target = find(index, query);
 64                    if(target!=null) {
 65                        // found
 66                        rsp.sendRedirect2(a.getUrl()+target.getUrl());
 67                        return;
 68                    }
 69                }
 70            }
 71        }
 72
 73        // no exact match. show the suggestions
 74        rsp.setStatus(SC_NOT_FOUND);
 75        req.getView(this,"search-failed.jelly").forward(req,rsp);
 76    }
 77
 78    /**
 79     * Used by OpenSearch auto-completion. Returns JSON array of the form:
 80     *
 81     * <pre>
 82     * ["queryString",["comp1","comp2",...]]
 83     * </pre>
 84     *
 85     * See http://developer.mozilla.org/en/docs/Supporting_search_suggestions_in_search_plugins
 86     */
 87    public void doSuggestOpenSearch(StaplerRequest req, StaplerResponse rsp, @QueryParameter String q) throws IOException, ServletException {
 88        DataWriter w = Flavor.JSON.createDataWriter(null, rsp);
 89        w.startArray();
 90        w.value(q);
 91
 92        w.startArray();
 93        for (SuggestedItem item : getSuggestions(req, q))
 94            w.value(item.getPath());
 95        w.endArray();
 96        w.endArray();
 97    }
 98
 99    /**
100     * Used by search box auto-completion. Returns JSON array.
101     */
102    public void doSuggest(StaplerRequest req, StaplerResponse rsp, @QueryParameter String query) throws IOException, ServletException {
103        Result r = new Result();
104        for (SuggestedItem item : getSuggestions(req, query))
105            r.suggestions.add(new Item(item.getPath()));
106
107        rsp.serveExposedBean(req,r,Flavor.JSON);
108    }
109
110    /**
111     * Gets the list of suggestions that match the given query.
112     *
113     * @return
114     *      can be empty but never null. The size of the list is always smaller than
115     *      a certain threshold to avoid showing too many options. 
116     */
117    public List<SuggestedItem> getSuggestions(StaplerRequest req, String query) {
118        Set<String> paths = new HashSet<String>();  // paths already added, to control duplicates
119        List<SuggestedItem> r = new ArrayList<SuggestedItem>();
120        for (SuggestedItem i : suggest(makeSuggestIndex(req), query)) {
121            if(r.size()>20) break;
122            if(paths.add(i.getPath()))
123                r.add(i);
124        }
125        return r;
126    }
127
128    /**
129     * Creates merged search index for suggestion.
130     */
131    private SearchIndex makeSuggestIndex(StaplerRequest req) {
132        SearchIndexBuilder builder = new SearchIndexBuilder();
133        for (Ancestor a : req.getAncestors()) {
134            if (a.getObject() instanceof SearchableModelObject) {
135                SearchableModelObject smo = (SearchableModelObject) a.getObject();
136                builder.add(smo.getSearchIndex());
137            }
138        }
139        return builder.make();
140    }
141
142    @ExportedBean
143    public static class Result {
144        //TODO: review and check whether we can do it private
145        @Exported
146        public List<Item> suggestions = new ArrayList<Item>();
147
148        public List<Item> getSuggestions() {
149            return suggestions;
150        }
151    }
152
153    @ExportedBean(defaultVisibility=999)
154    public static class Item {
155        @Exported
156        public String name;
157
158        public Item(String name) {
159            this.name = name;
160        }
161    }
162
163    private enum Mode {
164        FIND {
165            void find(SearchIndex index, String token, List<SearchItem> result) {
166                index.find(token, result);
167            }
168        },
169        SUGGEST {
170            void find(SearchIndex index, String token, List<SearchItem> result) {
171                index.suggest(token, result);
172            }
173        };
174
175        abstract void find(SearchIndex index, String token, List<SearchItem> result);
176
177    }
178
179    /**
180     * Performs a search and returns the match, or null if no match was found.
181     */
182    public static SuggestedItem find(SearchIndex index, String query) {
183        List<SuggestedItem> r = find(Mode.FIND, index, query);
184        if(r.isEmpty()) return null;
185        else            return r.get(0);
186    }
187
188    public static List<SuggestedItem> suggest(SearchIndex index, final String tokenList) {
189
190        class Tag implements Comparable<Tag>{
191            final SuggestedItem item;
192            final int distance;
193            /** If the path to this suggestion starts with the token list, 1. Otherwise 0. */
194            final int prefixMatch;
195
196            Tag(SuggestedItem i) {
197                item = i;
198                distance = EditDistance.editDistance(i.getPath(),tokenList);
199                prefixMatch = i.getPath().startsWith(tokenList)?1:0;
200            }
201
202            public int compareTo(Tag that) {
203                int r = this.prefixMatch -that.prefixMatch;
204                if(r!=0)    return -r;  // ones with head match should show up earlier
205                return this.distance-that.distance;
206            }
207        }
208
209        List<Tag> buf = new ArrayList<Tag>();
210        List<SuggestedItem> items = find(Mode.SUGGEST, index, tokenList);
211
212        // sort them
213        for( SuggestedItem i : items)
214            buf.add(new Tag(i));
215        Collections.sort(buf);
216        items.clear();
217        for (Tag t : buf)
218            items.add(t.item);
219
220        return items;
221    }
222
223    static final class TokenList {
224        private final String[] tokens;
225
226        public TokenList(String tokenList) {
227            tokens = tokenList.split("(?<=\\s)(?=\\S)");
228        }
229
230        public int length() { return tokens.length; }
231
232        /**
233         * Returns {@link List} such that its <tt>get(end)</tt>
234         * returns the concatanation of [token_start,...,token_end]
235         * (both end inclusive.)
236         */
237        public List<String> subSequence(final int start) {
238            return new AbstractList<String>() {
239                public String get(int index) {
240                    StringBuilder buf = new StringBuilder();
241                    for(int i=start; i<=start+index; i++ )
242                        buf.append(tokens[i]);
243                    return buf.toString().trim();
244                }
245
246                public int size() {
247                    return tokens.length-start;
248                }
249            };
250        }
251    }
252
253    private static List<SuggestedItem> find(Mode m, SearchIndex index, String tokenList) {
254        TokenList tokens = new TokenList(tokenList);
255        if(tokens.length()==0) return Collections.emptyList();   // no tokens given
256
257        List<SuggestedItem>[] paths = new List[tokens.length()+1]; // we won't use [0].
258        for(int i=1;i<=tokens.length();i++)
259            paths[i] = new ArrayList<SuggestedItem>();
260
261        List<SearchItem> items = new ArrayList<SearchItem>(); // items found in 1 step
262
263
264        // first token
265        int w=1;    // width of token
266        for (String token : tokens.subSequence(0)) {
267            items.clear();
268            m.find(index,token,items);
269            for (SearchItem si : items)
270                paths[w].add(new SuggestedItem(si));
271            w++;
272        }
273
274        // successive tokens
275        for (int j=1; j<tokens.length(); j++) {
276            // for each length
277            w=1;
278            for (String token : tokens.subSequence(j)) {
279                // for each candidate
280                for (SuggestedItem r : paths[j]) {
281                    items.clear();
282                    m.find(r.item.getSearchIndex(),token,items);
283                    for (SearchItem i : items)
284                        paths[j+w].add(new SuggestedItem(r,i));
285                }
286                w++;
287            }
288        }
289
290        return paths[tokens.length()];
291    }
292}