Search.java 7.8 KB
Newer Older
1 2
package hudson.search;

K
bug fix  
kohsuke 已提交
3
import hudson.util.EditDistance;
K
kohsuke 已提交
4
import org.kohsuke.stapler.Ancestor;
5
import org.kohsuke.stapler.QueryParameter;
K
kohsuke 已提交
6 7
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
8
import org.kohsuke.stapler.export.Exported;
9 10
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.export.Flavor;
11
import org.kohsuke.stapler.export.DataWriter;
K
bug fix  
kohsuke 已提交
12

13
import javax.servlet.ServletException;
K
kohsuke 已提交
14
import java.io.IOException;
15
import java.util.AbstractList;
16
import java.util.ArrayList;
K
bug fix  
kohsuke 已提交
17
import java.util.Collections;
18
import java.util.HashSet;
19
import java.util.List;
20
import java.util.Set;
21 22

/**
K
bug fix  
kohsuke 已提交
23 24
 * Web-bound object that serves QuickSilver-like search requests.
 *
25 26 27
 * @author Kohsuke Kawaguchi
 */
public class Search {
K
bug fix  
kohsuke 已提交
28
    public void doIndex(StaplerRequest req, StaplerResponse rsp) throws IOException {
29 30 31
        List<Ancestor> l = req.getAncestors();
        for( int i=l.size()-1; i>=0; i-- ) {
            Ancestor a = l.get(i);
K
bug fix  
kohsuke 已提交
32 33 34 35 36 37 38 39
            if (a.getObject() instanceof SearchableModelObject) {
                SearchableModelObject smo = (SearchableModelObject) a.getObject();

                SearchIndex index = smo.getSearchIndex();
                String query = req.getParameter("q");
                SuggestedItem target = find(index, query);
                if(target!=null) {
                    // found
K
kohsuke 已提交
40
                    rsp.sendRedirect2(a.getUrl()+target.getUrl());
K
kohsuke 已提交
41
                    return;
K
bug fix  
kohsuke 已提交
42 43 44 45 46 47 48 49
                }
            }
        }

        // TODO: go to suggestion page
        throw new UnsupportedOperationException();
    }

50
    /**
51 52 53 54 55
     * Used by OpenSearch auto-completion. Returns JSON array of the form:
     *
     * <pre>
     * ["queryString",["comp1","comp2",...]]
     * </pre>
K
kohsuke 已提交
56 57
     *
     * See http://developer.mozilla.org/en/docs/Supporting_search_suggestions_in_search_plugins
58
     */
59 60 61 62 63 64 65 66 67 68 69 70 71
    public void doSuggestOpenSearch(StaplerRequest req, StaplerResponse rsp, @QueryParameter("q")String query) throws IOException, ServletException {
        DataWriter w = Flavor.JSON.createDataWriter(null, rsp);
        w.startArray();
        w.value(query);

        w.startArray();
        Set<String> paths = new HashSet<String>();  // paths already added, to control duplicates
        for (SuggestedItem item : suggest(makeSuggestIndex(req), query)) {
            if(paths.size()>20) break;
            
            String p = item.getPath();
            if(paths.add(p))
                w.value(p);
72
        }
73 74 75
        w.endArray();
        w.endArray();
    }
76

77 78 79 80
    /**
     * Used by search box auto-completion. Returns JSON array.
     */
    public void doSuggest(StaplerRequest req, StaplerResponse rsp, @QueryParameter("query")String query) throws IOException, ServletException {
81
        Result r = new Result();
82
        Set<String> paths = new HashSet<String>();  // paths already added, to control duplicates
83 84
        for (SuggestedItem item : suggest(makeSuggestIndex(req), query)) {
            if(paths.size()>20) break;
85 86 87 88 89

            String p = item.getPath();
            if(paths.add(p))
                r.suggestions.add(new Item(p));
        }
90 91 92 93

        rsp.serveExposedBean(req,r,Flavor.JSON);
    }

94 95 96 97 98 99 100 101 102 103 104 105 106 107
    /**
     * Creates merged search index for suggestion.
     */
    private SearchIndex makeSuggestIndex(StaplerRequest req) {
        SearchIndexBuilder builder = new SearchIndexBuilder();
        for (Ancestor a : req.getAncestors()) {
            if (a.getObject() instanceof SearchableModelObject) {
                SearchableModelObject smo = (SearchableModelObject) a.getObject();
                builder.add(smo.getSearchIndex());
            }
        }
        return builder.make();
    }

108 109 110
    @ExportedBean
    public static class Result {
        @Exported
111
        public List<Item> suggestions = new ArrayList<Item>();
112 113 114 115 116 117 118 119 120 121 122 123
    }

    @ExportedBean(defaultVisibility=999)
    public static class Item {
        @Exported
        public String name;

        public Item(String name) {
            this.name = name;
        }
    }

124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
    private enum Mode {
        FIND {
            void find(SearchIndex index, String token, List<SearchItem> result) {
                index.find(token, result);
            }
        },
        SUGGEST {
            void find(SearchIndex index, String token, List<SearchItem> result) {
                index.suggest(token, result);
            }
        };

        abstract void find(SearchIndex index, String token, List<SearchItem> result);

    }

K
bug fix  
kohsuke 已提交
140 141 142 143 144
    /**
     * Performs a search and returns the match, or null if no match was found.
     */
    public static SuggestedItem find(SearchIndex index, String query) {
        List<SuggestedItem> r = find(Mode.FIND, index, query);
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
        if(r.isEmpty()) return null;
        else            return r.get(0);
    }

    public static List<SuggestedItem> suggest(SearchIndex index, final String tokenList) {

        class Tag implements Comparable<Tag>{
            SuggestedItem item;
            int distance;

            Tag(SuggestedItem i) {
                this.item = i;
                distance = EditDistance.editDistance(i.getPath(),tokenList);
            }

            public int compareTo(Tag that) {
                return this.distance-that.distance;
            }
        }

        List<Tag> buf = new ArrayList<Tag>();
        List<SuggestedItem> items = find(Mode.SUGGEST, index, tokenList);

        // sort them
        for( SuggestedItem i : items)
            buf.add(new Tag(i));
        Collections.sort(buf);
        items.clear();
        for (Tag t : buf)
            items.add(t.item);

        return items;
    }

179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
    static final class TokenList {
        private final String[] tokens;

        public TokenList(String tokenList) {
            tokens = tokenList.split("(?<=\\s)(?=\\S)");
        }

        public int length() { return tokens.length; }

        /**
         * Returns {@link List} such that its <tt>get(end)</tt>
         * returns the concatanation of [token_start,...,token_end]
         * (both end inclusive.)
         */
        public List<String> subSequence(final int start) {
            return new AbstractList<String>() {
                public String get(int index) {
                    StringBuilder buf = new StringBuilder();
                    for(int i=start; i<=start+index; i++ )
                        buf.append(tokens[i]);
                    return buf.toString().trim();
                }

                public int size() {
                    return tokens.length-start;
                }
            };
        }
    }

209
    private static List<SuggestedItem> find(Mode m, SearchIndex index, String tokenList) {
210 211 212 213 214 215 216
        TokenList tokens = new TokenList(tokenList);
        if(tokens.length()==0) return Collections.emptyList();   // no tokens given

        List<SuggestedItem>[] paths = new List[tokens.length()+1]; // we won't use [0].
        for(int i=1;i<=tokens.length();i++)
            paths[i] = new ArrayList<SuggestedItem>();

217 218 219 220
        List<SearchItem> items = new ArrayList<SearchItem>(); // items found in 1 step


        // first token
221 222 223 224 225 226 227 228
        int w=1;    // width of token
        for (String token : tokens.subSequence(0)) {
            items.clear();
            m.find(index,token,items);
            for (SearchItem si : items)
                paths[w].add(new SuggestedItem(si));
            w++;
        }
229 230

        // successive tokens
231 232 233 234 235 236 237 238 239 240 241 242
        for (int j=1; j<tokens.length(); j++) {
            // for each length
            w=1;
            for (String token : tokens.subSequence(j)) {
                // for each candidate
                for (SuggestedItem r : paths[j]) {
                    items.clear();
                    m.find(r.item.getSearchIndex(),token,items);
                    for (SearchItem i : items)
                        paths[j+w].add(new SuggestedItem(r,i));
                }
                w++;
243 244 245
            }
        }

246
        return paths[tokens.length()];
247 248
    }
}