| // Copyright 2016 Google Inc. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package rest |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "io/ioutil" |
| "net/http" |
| |
| "github.com/google/zoekt" |
| "github.com/google/zoekt/query" |
| "golang.org/x/net/context" |
| ) |
| |
| const jsonContentType = "application/json; charset=utf-8" |
| |
| type httpError struct { |
| msg string |
| status int |
| } |
| |
| func (e *httpError) Error() string { return fmt.Sprintf("%d: %s", e.status, e.msg) } |
| |
| func Search(s zoekt.Searcher, w http.ResponseWriter, r *http.Request) { |
| if err := serveSearchAPIErr(s, w, r); err != nil { |
| if e, ok := err.(*httpError); ok { |
| http.Error(w, e.msg, e.status) |
| } |
| http.Error(w, err.Error(), http.StatusTeapot) |
| } |
| } |
| |
| func serveSearchAPIErr(s zoekt.Searcher, w http.ResponseWriter, r *http.Request) error { |
| if r.Method != http.MethodPost { |
| return &httpError{"must use POST", http.StatusMethodNotAllowed} |
| } |
| |
| if got := r.Header.Get("Content-Type"); got != jsonContentType { |
| return &httpError{"must use " + jsonContentType, http.StatusNotAcceptable} |
| |
| } |
| |
| content, err := ioutil.ReadAll(r.Body) |
| if err != nil { |
| return &httpError{err.Error(), http.StatusBadRequest} |
| } |
| |
| var req SearchRequest |
| if err := json.Unmarshal(content, &req); err != nil { |
| return &httpError{err.Error(), http.StatusBadRequest} |
| } |
| |
| rep, err := serveSearchAPIStructured(s, &req) |
| if err != nil { |
| return err |
| } |
| content, err = json.Marshal(rep) |
| if err != nil { |
| return &httpError{err.Error(), http.StatusInternalServerError} |
| |
| } |
| |
| w.Header().Set("Content-Type", jsonContentType) |
| if _, err := w.Write(content); err != nil { |
| return &httpError{err.Error(), http.StatusInternalServerError} |
| } |
| return nil |
| } |
| |
| func serveSearchAPIStructured(searcher zoekt.Searcher, req *SearchRequest) (*SearchResponse, error) { |
| q, err := query.Parse(req.Query) |
| if err != nil { |
| msg := "parse error: " + err.Error() |
| return &SearchResponse{Error: &msg}, nil |
| } |
| |
| var restrictions []query.Q |
| for _, r := range req.Restrict { |
| var branchQs []query.Q |
| for _, b := range r.Branches { |
| branchQs = append(branchQs, &query.Branch{b}) |
| } |
| |
| restrictions = append(restrictions, |
| query.NewAnd(&query.Repo{r.Repo}, query.NewOr(branchQs...))) |
| } |
| |
| finalQ := query.NewAnd(q, query.NewOr(restrictions...)) |
| var options zoekt.SearchOptions |
| options.SetDefaults() |
| |
| ctx := context.Background() |
| result, err := searcher.Search(ctx, finalQ, &options) |
| if err != nil { |
| return nil, &httpError{err.Error(), http.StatusInternalServerError} |
| } |
| |
| // TODO - make this tunable. Use a query param or a JSON struct? |
| num := 50 |
| if len(result.Files) > num { |
| result.Files = result.Files[:num] |
| } |
| var resp SearchResponse |
| for _, f := range result.Files { |
| srf := SearchResponseFile{ |
| Repo: f.Repository, |
| Branches: f.Branches, |
| FileName: f.FileName, |
| // TODO - set version |
| } |
| for _, m := range f.LineMatches { |
| srl := &SearchResponseLine{ |
| LineNumber: m.LineNumber, |
| Line: string(m.Line), |
| } |
| |
| // Convert to unicode indices. |
| charOffsets := make([]int, len(m.Line), len(m.Line)+1) |
| j := 0 |
| for i := range srl.Line { |
| charOffsets[i] = j |
| j++ |
| } |
| charOffsets = append(charOffsets, j) |
| |
| for _, fr := range m.LineFragments { |
| srfr := SearchResponseMatch{ |
| Start: charOffsets[fr.LineOffset], |
| End: charOffsets[fr.LineOffset+fr.MatchLength], |
| } |
| |
| srl.Matches = append(srl.Matches, &srfr) |
| } |
| srf.Lines = append(srf.Lines, srl) |
| } |
| resp.Files = append(resp.Files, &srf) |
| } |
| |
| return &resp, nil |
| } |