2011-03-13 23:38:32 +00:00
|
|
|
/*
|
|
|
|
Copyright 2011 Google Inc.
|
|
|
|
|
|
|
|
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 search
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"http"
|
2011-03-14 03:51:58 +00:00
|
|
|
"log"
|
2011-03-14 00:14:48 +00:00
|
|
|
"os"
|
2011-03-14 03:51:58 +00:00
|
|
|
"sort"
|
2011-05-30 22:44:25 +00:00
|
|
|
"strings"
|
2011-03-14 02:27:59 +00:00
|
|
|
"sync"
|
2011-03-14 00:14:48 +00:00
|
|
|
"time"
|
2011-05-30 05:52:31 +00:00
|
|
|
|
|
|
|
"camli/blobref"
|
|
|
|
"camli/blobserver"
|
|
|
|
"camli/jsonconfig"
|
|
|
|
"camli/httputil"
|
2011-03-13 23:38:32 +00:00
|
|
|
)
|
|
|
|
|
2011-05-30 05:52:31 +00:00
|
|
|
func init() {
|
|
|
|
blobserver.RegisterHandlerConstructor("search", newHandlerFromConfig)
|
|
|
|
}
|
|
|
|
|
|
|
|
type searchHandler struct {
|
|
|
|
index Index
|
|
|
|
owner *blobref.BlobRef
|
|
|
|
}
|
|
|
|
|
|
|
|
func newHandlerFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (http.Handler, os.Error) {
|
|
|
|
indexPrefix := conf.RequiredString("index") // TODO: add optional help tips here?
|
|
|
|
ownerBlobStr := conf.RequiredString("owner")
|
|
|
|
if err := conf.Validate(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
indexHandler, err := ld.GetHandler(indexPrefix)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("search config references unknown handler %q", indexPrefix)
|
2011-03-13 23:38:32 +00:00
|
|
|
}
|
2011-05-30 05:52:31 +00:00
|
|
|
indexer, ok := indexHandler.(Index)
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("search config references invalid indexer %q (actually a %T)", indexPrefix, indexHandler)
|
|
|
|
}
|
|
|
|
ownerBlobRef := blobref.Parse(ownerBlobStr)
|
|
|
|
if ownerBlobRef == nil {
|
|
|
|
return nil, fmt.Errorf("search 'owner' has malformed blobref %q; expecting e.g. sha1-xxxxxxxxxxxx",
|
|
|
|
ownerBlobStr)
|
|
|
|
}
|
|
|
|
return &searchHandler{
|
|
|
|
index: indexer,
|
|
|
|
owner: ownerBlobRef,
|
|
|
|
}, nil
|
2011-03-13 23:38:32 +00:00
|
|
|
}
|
|
|
|
|
2011-03-14 00:14:48 +00:00
|
|
|
|
2011-06-09 19:55:38 +00:00
|
|
|
func jsonMap() map[string]interface{} {
|
|
|
|
return make(map[string]interface{})
|
|
|
|
}
|
|
|
|
|
|
|
|
func jsonMapList() []map[string]interface{} {
|
|
|
|
return make([]map[string]interface{}, 0)
|
|
|
|
}
|
2011-03-14 02:27:59 +00:00
|
|
|
|
2011-05-30 22:44:25 +00:00
|
|
|
func (sh *searchHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|
|
|
_ = req.Header.Get("X-PrefixHandler-PathBase")
|
|
|
|
suffix := req.Header.Get("X-PrefixHandler-PathSuffix")
|
|
|
|
|
2011-06-09 19:55:38 +00:00
|
|
|
if req.Method == "GET" {
|
|
|
|
switch suffix {
|
|
|
|
case "camli/search", "camli/search/recent":
|
|
|
|
sh.serveRecentPermanodes(rw, req)
|
|
|
|
return
|
|
|
|
case "camli/search/describe":
|
|
|
|
sh.serveDescribe(rw, req)
|
|
|
|
return
|
|
|
|
case "camli/search/claims":
|
|
|
|
sh.serveClaims(rw, req)
|
|
|
|
return
|
|
|
|
case "camli/search/files":
|
|
|
|
sh.serveFiles(rw, req)
|
|
|
|
return
|
|
|
|
}
|
2011-05-30 22:44:25 +00:00
|
|
|
}
|
|
|
|
|
2011-06-09 19:55:38 +00:00
|
|
|
// TODO: discovery for the endpoints & better error message with link to discovery info
|
|
|
|
ret := jsonMap()
|
|
|
|
ret["error"] = "Unsupported search path or method"
|
|
|
|
ret["errorType"] = "input"
|
|
|
|
httputil.ReturnJson(rw, ret)
|
2011-05-30 22:44:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (sh *searchHandler) serveRecentPermanodes(rw http.ResponseWriter, req *http.Request) {
|
2011-03-14 00:14:48 +00:00
|
|
|
ch := make(chan *Result)
|
2011-06-09 19:55:38 +00:00
|
|
|
results := jsonMapList()
|
2011-03-14 00:14:48 +00:00
|
|
|
errch := make(chan os.Error)
|
|
|
|
go func() {
|
2011-05-30 05:52:31 +00:00
|
|
|
log.Printf("finding recent permanodes for %s", sh.owner)
|
|
|
|
errch <- sh.index.GetRecentPermanodes(ch, []*blobref.BlobRef{sh.owner}, 50)
|
2011-03-14 00:14:48 +00:00
|
|
|
}()
|
2011-03-14 02:27:59 +00:00
|
|
|
|
|
|
|
wg := new(sync.WaitGroup)
|
2011-03-14 00:14:48 +00:00
|
|
|
for res := range ch {
|
2011-06-09 19:55:38 +00:00
|
|
|
jm := jsonMap()
|
2011-03-14 00:14:48 +00:00
|
|
|
jm["blobref"] = res.BlobRef.String()
|
2011-03-14 02:27:59 +00:00
|
|
|
jm["owner"] = res.Signer.String()
|
2011-03-14 00:14:48 +00:00
|
|
|
t := time.SecondsToUTC(res.LastModTime)
|
|
|
|
jm["modtime"] = t.Format(time.RFC3339)
|
|
|
|
results = append(results, jm)
|
2011-03-14 02:27:59 +00:00
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
2011-05-30 22:44:25 +00:00
|
|
|
sh.populatePermanodeFields(jm, res.BlobRef, res.Signer, nil)
|
2011-03-14 02:27:59 +00:00
|
|
|
wg.Done()
|
|
|
|
}()
|
2011-03-14 00:14:48 +00:00
|
|
|
}
|
2011-03-14 02:27:59 +00:00
|
|
|
wg.Wait()
|
|
|
|
|
2011-03-14 00:14:48 +00:00
|
|
|
err := <-errch
|
|
|
|
|
2011-06-09 19:55:38 +00:00
|
|
|
ret := jsonMap()
|
2011-03-14 00:14:48 +00:00
|
|
|
ret["results"] = results
|
2011-03-14 02:27:59 +00:00
|
|
|
if err != nil {
|
|
|
|
// TODO: return error status code
|
|
|
|
ret["error"] = fmt.Sprintf("%v", err)
|
|
|
|
}
|
2011-05-30 22:44:25 +00:00
|
|
|
httputil.ReturnJson(rw, ret)
|
|
|
|
}
|
|
|
|
|
2011-06-03 22:23:23 +00:00
|
|
|
func (sh *searchHandler) serveClaims(rw http.ResponseWriter, req *http.Request) {
|
2011-06-09 19:55:38 +00:00
|
|
|
ret := jsonMap()
|
2011-06-03 22:23:23 +00:00
|
|
|
|
|
|
|
pn := blobref.Parse(req.FormValue("permanode"))
|
|
|
|
if pn == nil {
|
|
|
|
http.Error(rw, "Missing or invalid 'permanode' param", 400)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: rename GetOwnerClaims to GetClaims?
|
|
|
|
claims, err := sh.index.GetOwnerClaims(pn, sh.owner)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Error getting claims of %s: %v", pn.String(), err)
|
|
|
|
} else {
|
|
|
|
sort.Sort(claims)
|
2011-06-09 19:55:38 +00:00
|
|
|
jclaims := jsonMapList()
|
2011-06-03 22:23:23 +00:00
|
|
|
|
|
|
|
for _, claim := range claims {
|
2011-06-09 19:55:38 +00:00
|
|
|
jclaim := jsonMap()
|
2011-06-03 22:23:23 +00:00
|
|
|
jclaim["blobref"] = claim.BlobRef.String()
|
|
|
|
jclaim["signer"] = claim.Signer.String()
|
|
|
|
jclaim["permanode"] = claim.Permanode.String()
|
|
|
|
jclaim["date"] = claim.Date.Format(time.RFC3339)
|
|
|
|
jclaim["type"] = claim.Type
|
|
|
|
if claim.Attr != "" {
|
|
|
|
jclaim["attr"] = claim.Attr
|
|
|
|
}
|
|
|
|
if claim.Value != "" {
|
|
|
|
jclaim["value"] = claim.Value
|
|
|
|
}
|
|
|
|
|
|
|
|
jclaims = append(jclaims, jclaim)
|
|
|
|
}
|
|
|
|
ret["claims"] = jclaims
|
|
|
|
}
|
|
|
|
|
|
|
|
httputil.ReturnJson(rw, ret)
|
|
|
|
}
|
|
|
|
|
2011-05-30 22:44:25 +00:00
|
|
|
func (sh *searchHandler) serveDescribe(rw http.ResponseWriter, req *http.Request) {
|
2011-06-09 19:55:38 +00:00
|
|
|
ret := jsonMap()
|
2011-06-11 16:29:41 +00:00
|
|
|
defer httputil.ReturnJson(rw, ret)
|
|
|
|
|
2011-05-30 22:44:25 +00:00
|
|
|
br := blobref.Parse(req.FormValue("blobref"))
|
|
|
|
if br == nil {
|
2011-06-11 16:29:41 +00:00
|
|
|
ret["error"] = "Missing or invalid 'blobref' param"
|
|
|
|
ret["errorType"] = "input"
|
2011-05-30 22:44:25 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2011-06-09 19:55:38 +00:00
|
|
|
dmap := func(b *blobref.BlobRef) map[string]interface{} {
|
2011-05-30 22:44:25 +00:00
|
|
|
bs := b.String()
|
|
|
|
if m, ok := ret[bs]; ok {
|
2011-06-09 19:55:38 +00:00
|
|
|
return m.(map[string]interface{})
|
2011-05-30 22:44:25 +00:00
|
|
|
}
|
2011-06-09 19:55:38 +00:00
|
|
|
m := jsonMap()
|
2011-05-30 22:44:25 +00:00
|
|
|
ret[bs] = m
|
|
|
|
return m
|
|
|
|
}
|
|
|
|
|
|
|
|
mime, size, err := sh.index.GetBlobMimeType(br)
|
2011-05-30 23:41:56 +00:00
|
|
|
if err != os.ENOENT {
|
|
|
|
if err != nil {
|
|
|
|
ret["errorText"] = err.String()
|
|
|
|
} else {
|
|
|
|
m := dmap(br)
|
|
|
|
setMimeType(m, mime)
|
|
|
|
m["size"] = size
|
|
|
|
|
|
|
|
if mime == "application/json; camliType=permanode" {
|
2011-06-09 19:55:38 +00:00
|
|
|
pm := jsonMap()
|
2011-05-30 23:41:56 +00:00
|
|
|
m["permanode"] = pm
|
|
|
|
sh.populatePermanodeFields(pm, br, sh.owner, dmap)
|
|
|
|
}
|
2011-05-30 22:44:25 +00:00
|
|
|
}
|
|
|
|
}
|
2011-03-13 23:38:32 +00:00
|
|
|
}
|
2011-03-14 02:27:59 +00:00
|
|
|
|
2011-06-09 19:55:38 +00:00
|
|
|
func (sh *searchHandler) serveFiles(rw http.ResponseWriter, req *http.Request) {
|
|
|
|
ret := jsonMap()
|
|
|
|
defer httputil.ReturnJson(rw, ret)
|
|
|
|
|
|
|
|
br := blobref.Parse(req.FormValue("bytesref"))
|
|
|
|
if br == nil {
|
|
|
|
// TODO: formalize how errors are returned And make
|
|
|
|
// ReturnJson set the HTTP status to 400 automatically
|
|
|
|
// in some cases, if errorType is "input"? Document
|
|
|
|
// this somewhere. Are there existing JSON
|
|
|
|
// conventions to use?
|
|
|
|
ret["error"] = "Missing or invalid 'bytesref' param"
|
|
|
|
ret["errorType"] = "input"
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
files, err := sh.index.ExistingFileSchemas(br)
|
|
|
|
if err != nil {
|
|
|
|
ret["error"] = err.String()
|
|
|
|
ret["errorType"] = "server"
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
strList := []string{}
|
|
|
|
for _, br := range files {
|
|
|
|
strList = append(strList, br.String())
|
|
|
|
}
|
|
|
|
ret["files"] = strList
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2011-05-30 22:44:25 +00:00
|
|
|
// dmap may be nil, returns the jsonMap to populate into
|
2011-06-09 19:55:38 +00:00
|
|
|
func (sh *searchHandler) populatePermanodeFields(jm map[string]interface{}, pn, signer *blobref.BlobRef, dmap func(b *blobref.BlobRef) map[string]interface{}) {
|
|
|
|
attr := jsonMap()
|
2011-03-14 02:27:59 +00:00
|
|
|
jm["attr"] = attr
|
2011-03-14 03:51:58 +00:00
|
|
|
|
2011-05-30 22:44:25 +00:00
|
|
|
claims, err := sh.index.GetOwnerClaims(pn, signer)
|
2011-03-14 03:51:58 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Printf("Error getting claims of %s: %v", pn.String(), err)
|
2011-06-11 16:51:08 +00:00
|
|
|
jm["error"] = fmt.Sprintf("Error getting claims of %s: %v", pn.String(), err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
sort.Sort(claims)
|
|
|
|
claimLoop:
|
|
|
|
for _, cl := range claims {
|
|
|
|
switch cl.Type {
|
|
|
|
case "del-attribute":
|
|
|
|
if cl.Value == "" {
|
2011-03-14 03:51:58 +00:00
|
|
|
attr[cl.Attr] = nil, false
|
2011-06-11 16:51:08 +00:00
|
|
|
} else {
|
2011-03-14 03:51:58 +00:00
|
|
|
sl, ok := attr[cl.Attr].([]string)
|
2011-06-04 17:18:38 +00:00
|
|
|
if ok {
|
2011-06-11 16:51:08 +00:00
|
|
|
filtered := make([]string, 0, len(sl))
|
|
|
|
for _, val := range sl {
|
|
|
|
if val != cl.Value {
|
|
|
|
filtered = append(filtered, val)
|
2011-06-04 17:18:38 +00:00
|
|
|
}
|
|
|
|
}
|
2011-06-11 16:51:08 +00:00
|
|
|
attr[cl.Attr] = filtered
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case "set-attribute":
|
|
|
|
attr[cl.Attr] = nil, false
|
|
|
|
fallthrough
|
|
|
|
case "add-attribute":
|
|
|
|
if cl.Value == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
sl, ok := attr[cl.Attr].([]string)
|
|
|
|
if ok {
|
|
|
|
for _, exist := range sl {
|
|
|
|
if exist == cl.Value {
|
|
|
|
continue claimLoop
|
|
|
|
}
|
2011-03-14 03:51:58 +00:00
|
|
|
}
|
2011-06-11 16:51:08 +00:00
|
|
|
} else {
|
|
|
|
sl = make([]string, 0, 1)
|
|
|
|
attr[cl.Attr] = sl
|
2011-03-14 03:51:58 +00:00
|
|
|
}
|
2011-06-11 16:51:08 +00:00
|
|
|
attr[cl.Attr] = append(sl, cl.Value)
|
2011-03-14 03:51:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the content permanode is now known, look up its type
|
2011-06-11 16:51:08 +00:00
|
|
|
if content, ok := attr["camliContent"].([]string); ok && len(content) > 0 {
|
|
|
|
cbr := blobref.Parse(content[len(content)-1])
|
2011-05-30 22:44:25 +00:00
|
|
|
|
|
|
|
dm := jm
|
|
|
|
if dmap != nil {
|
|
|
|
dm = dmap(cbr)
|
2011-03-14 03:51:58 +00:00
|
|
|
}
|
2011-05-30 22:44:25 +00:00
|
|
|
|
|
|
|
mime, size, err := sh.index.GetBlobMimeType(cbr)
|
|
|
|
if err == nil {
|
|
|
|
setMimeType(dm, mime)
|
|
|
|
dm["size"] = size
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const camliTypePrefix = "application/json; camliType="
|
|
|
|
|
2011-06-09 19:55:38 +00:00
|
|
|
func setMimeType(m map[string]interface{}, mime string) {
|
2011-05-30 22:44:25 +00:00
|
|
|
m["type"] = mime
|
|
|
|
if strings.HasPrefix(mime, camliTypePrefix) {
|
|
|
|
m["camliType"] = mime[len(camliTypePrefix):]
|
2011-03-14 03:51:58 +00:00
|
|
|
}
|
2011-03-14 02:27:59 +00:00
|
|
|
}
|