github.com/cayleygraph/cayley@v0.7.7/server/http/api_v2.go (about) 1 // Copyright 2017 The Cayley Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package cayleyhttp 16 17 import ( 18 "compress/gzip" 19 "context" 20 "encoding/json" 21 "errors" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "net/http" 26 "strings" 27 "time" 28 29 "github.com/julienschmidt/httprouter" 30 31 "github.com/cayleygraph/cayley/clog" 32 "github.com/cayleygraph/cayley/graph" 33 "github.com/cayleygraph/cayley/graph/shape" 34 "github.com/cayleygraph/cayley/query" 35 _ "github.com/cayleygraph/cayley/writer" 36 "github.com/cayleygraph/quad" 37 ) 38 39 func NewAPIv2(h *graph.Handle, wrappers ...HandlerWrapper) *APIv2 { 40 return NewAPIv2Writer(h, "single", nil, wrappers...) 41 } 42 43 func NewAPIv2Writer(h *graph.Handle, wtype string, wopts graph.Options, wrappers ...HandlerWrapper) *APIv2 { 44 api := &APIv2{h: h, wtyp: wtype, wopt: wopts, limit: 100} 45 api.r = httprouter.New() 46 api.RegisterOn(api.r, wrappers...) 47 return api 48 } 49 50 type APIv2 struct { 51 h *graph.Handle 52 r *httprouter.Router 53 ro bool 54 batch int 55 56 // replication 57 wtyp string 58 wopt graph.Options 59 60 // query 61 timeout time.Duration 62 limit int 63 } 64 65 func (api *APIv2) SetReadOnly(ro bool) { 66 api.ro = ro 67 } 68 func (api *APIv2) SetBatchSize(n int) { 69 api.batch = n 70 } 71 func (api *APIv2) SetQueryTimeout(dt time.Duration) { 72 api.timeout = dt 73 } 74 func (api *APIv2) SetQueryLimit(n int) { 75 api.limit = n 76 } 77 func (api *APIv2) ServeHTTP(w http.ResponseWriter, r *http.Request) { 78 api.r.ServeHTTP(w, r) 79 } 80 81 type HandlerWrapper func(httprouter.Handle) httprouter.Handle 82 83 func wrap(h http.HandlerFunc, arr []HandlerWrapper) httprouter.Handle { 84 wh := func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 85 h(w, r) 86 } 87 for _, w := range arr { 88 wh = w(wh) 89 } 90 return wh 91 } 92 func (api *APIv2) RegisterDataOn(r *httprouter.Router, wrappers ...HandlerWrapper) { 93 if !api.ro { 94 r.POST("/api/v2/write", wrap(api.ServeWrite, wrappers)) 95 r.POST("/api/v2/delete", wrap(api.ServeDelete, wrappers)) 96 r.POST("/api/v2/node/delete", wrap(api.ServeNodeDelete, wrappers)) 97 } 98 r.POST("/api/v2/read", wrap(api.ServeRead, wrappers)) 99 r.GET("/api/v2/read", wrap(api.ServeRead, wrappers)) 100 r.GET("/api/v2/formats", wrap(api.ServeFormats, wrappers)) 101 } 102 func (api *APIv2) RegisterQueryOn(r *httprouter.Router, wrappers ...HandlerWrapper) { 103 r.POST("/api/v2/query", wrap(api.ServeQuery, wrappers)) 104 r.GET("/api/v2/query", wrap(api.ServeQuery, wrappers)) 105 } 106 func (api *APIv2) RegisterOn(r *httprouter.Router, wrappers ...HandlerWrapper) { 107 api.RegisterDataOn(r, wrappers...) 108 api.RegisterQueryOn(r, wrappers...) 109 } 110 111 const ( 112 defaultFormat = "nquads" 113 hdrContentType = "Content-Type" 114 hdrContentEncoding = "Content-Encoding" 115 hdrAccept = "Accept" 116 hdrAcceptEncoding = "Accept-Encoding" 117 contentTypeJSON = "application/json" 118 contentTypeJSONLD = "application/ld+json" 119 ) 120 121 func getFormat(r *http.Request, formKey string, acceptName string) *quad.Format { 122 var format *quad.Format 123 if formKey != "" { 124 if name := r.FormValue("format"); name != "" { 125 format = quad.FormatByName(name) 126 } 127 } 128 if acceptName != "" && format == nil { 129 specs := ParseAccept(r.Header, acceptName) 130 // TODO: sort by Q 131 if len(specs) != 0 { 132 format = quad.FormatByMime(specs[0].Value) 133 } 134 } 135 if format == nil { 136 format = quad.FormatByName(defaultFormat) 137 } 138 return format 139 } 140 141 func readerFrom(r *http.Request, acceptName string) (io.ReadCloser, error) { 142 if specs := ParseAccept(r.Header, acceptName); len(specs) != 0 { 143 if s := specs[0]; s.Value == "gzip" { 144 zr, err := gzip.NewReader(r.Body) 145 if err != nil { 146 return nil, err 147 } 148 return zr, nil 149 } 150 } 151 return r.Body, nil 152 } 153 154 type nopWriteCloser struct { 155 io.Writer 156 } 157 158 func (nopWriteCloser) Close() error { return nil } 159 160 func writerFrom(w http.ResponseWriter, r *http.Request, acceptName string) io.WriteCloser { 161 if specs := ParseAccept(r.Header, acceptName); len(specs) != 0 { 162 if s := specs[0]; s.Value == "gzip" { 163 w.Header().Set(hdrContentEncoding, s.Value) 164 zw := gzip.NewWriter(w) 165 return zw 166 } 167 } 168 return nopWriteCloser{Writer: w} 169 } 170 171 func (api *APIv2) handleForRequest(r *http.Request) (*graph.Handle, error) { 172 return HandleForRequest(api.h, api.wtyp, api.wopt, r) 173 } 174 175 func (api *APIv2) ServeWrite(w http.ResponseWriter, r *http.Request) { 176 defer r.Body.Close() 177 if api.ro { 178 jsonResponse(w, http.StatusForbidden, errors.New("database is read-only")) 179 return 180 } 181 format := getFormat(r, "", hdrContentType) 182 if format == nil || format.Reader == nil { 183 jsonResponse(w, http.StatusBadRequest, errors.New("format is not supported for reading data")) 184 return 185 } 186 rd, err := readerFrom(r, hdrContentEncoding) 187 if err != nil { 188 jsonResponse(w, http.StatusBadRequest, err) 189 return 190 } 191 defer rd.Close() 192 qr := format.Reader(rd) 193 defer qr.Close() 194 h, err := api.handleForRequest(r) 195 if err != nil { 196 jsonResponse(w, http.StatusBadRequest, err) 197 return 198 } 199 qw := graph.NewWriter(h.QuadWriter) 200 defer qw.Close() 201 n, err := quad.CopyBatch(qw, qr, api.batch) 202 if err != nil { 203 jsonResponse(w, http.StatusInternalServerError, err) 204 return 205 } 206 err = qw.Close() 207 if err != nil { 208 jsonResponse(w, http.StatusInternalServerError, err) 209 return 210 } 211 w.Header().Set(hdrContentType, contentTypeJSON) 212 fmt.Fprintf(w, `{"result": "Successfully wrote %d quads.", "count": %d}`+"\n", n, n) 213 } 214 215 func (api *APIv2) ServeDelete(w http.ResponseWriter, r *http.Request) { 216 defer r.Body.Close() 217 if api.ro { 218 jsonResponse(w, http.StatusForbidden, errors.New("database is read-only")) 219 return 220 } 221 format := getFormat(r, "", hdrContentType) 222 if format == nil || format.Reader == nil { 223 jsonResponse(w, http.StatusBadRequest, fmt.Errorf("format is not supported for reading quads")) 224 return 225 } 226 rd, err := readerFrom(r, hdrContentEncoding) 227 if err != nil { 228 jsonResponse(w, http.StatusBadRequest, err) 229 return 230 } 231 defer rd.Close() 232 qr := format.Reader(r.Body) 233 defer qr.Close() 234 h, err := api.handleForRequest(r) 235 if err != nil { 236 jsonResponse(w, http.StatusBadRequest, err) 237 return 238 } 239 qw := graph.NewRemover(h.QuadWriter) 240 defer qw.Close() 241 n, err := quad.CopyBatch(qw, qr, api.batch) 242 if err != nil { 243 jsonResponse(w, http.StatusInternalServerError, err) 244 return 245 } 246 w.Header().Set(hdrContentType, contentTypeJSON) 247 fmt.Fprintf(w, `{"result": "Successfully deleted %d quads.", "count": %d}`+"\n", n, n) 248 } 249 250 func (api *APIv2) ServeNodeDelete(w http.ResponseWriter, r *http.Request) { 251 defer r.Body.Close() 252 if api.ro { 253 jsonResponse(w, http.StatusForbidden, errors.New("database is read-only")) 254 return 255 } 256 format := getFormat(r, "", hdrContentType) 257 if format == nil || format.UnmarshalValue == nil { 258 jsonResponse(w, http.StatusBadRequest, fmt.Errorf("format is not supported for reading nodes")) 259 return 260 } 261 const limit = 128*1024 + 1 262 rd := io.LimitReader(r.Body, limit) 263 data, err := ioutil.ReadAll(rd) 264 if err != nil { 265 jsonResponse(w, http.StatusBadRequest, err) 266 return 267 } else if len(data) == limit { 268 jsonResponse(w, http.StatusBadRequest, fmt.Errorf("request data is too large")) 269 return 270 } 271 v, err := format.UnmarshalValue(data) 272 if err != nil { 273 jsonResponse(w, http.StatusBadRequest, err) 274 return 275 } else if v == nil { 276 jsonResponse(w, http.StatusBadRequest, fmt.Errorf("cannot remove nil value")) 277 return 278 } 279 h, err := api.handleForRequest(r) 280 if err != nil { 281 jsonResponse(w, http.StatusBadRequest, err) 282 return 283 } 284 err = h.RemoveNode(v) 285 if err != nil { 286 jsonResponse(w, http.StatusInternalServerError, err) 287 return 288 } 289 w.Header().Set(hdrContentType, contentTypeJSON) 290 const n = 1 291 fmt.Fprintf(w, `{"result": "Successfully deleted %d nodes.", "count": %d}`+"\n", n, n) 292 } 293 294 type checkWriter struct { 295 w io.Writer 296 written bool 297 } 298 299 func (w *checkWriter) Write(p []byte) (int, error) { 300 w.written = true 301 return w.w.Write(p) 302 } 303 304 func valuesFromString(s string) []quad.Value { 305 if s == "" { 306 return nil 307 } 308 arr := strings.Split(s, ",") 309 out := make([]quad.Value, 0, len(arr)) 310 for _, s := range arr { 311 out = append(out, quad.StringToValue(s)) 312 } 313 return out 314 } 315 316 func (api *APIv2) ServeRead(w http.ResponseWriter, r *http.Request) { 317 format := getFormat(r, "format", hdrAccept) 318 if format == nil || format.Writer == nil { 319 jsonResponse(w, http.StatusBadRequest, fmt.Errorf("format is not supported for reading data")) 320 return 321 } 322 h, err := api.handleForRequest(r) 323 if err != nil { 324 jsonResponse(w, http.StatusBadRequest, err) 325 return 326 } 327 values := shape.FilterQuads( 328 valuesFromString(r.FormValue("sub")), 329 valuesFromString(r.FormValue("pred")), 330 valuesFromString(r.FormValue("obj")), 331 valuesFromString(r.FormValue("label")), 332 ) 333 it := values.BuildIterator(h.QuadStore) 334 qr := graph.NewResultReader(h.QuadStore, it) 335 336 defer qr.Close() 337 338 wr := writerFrom(w, r, hdrAcceptEncoding) 339 defer wr.Close() 340 341 cw := &checkWriter{w: wr} 342 qwc := format.Writer(cw) 343 defer qwc.Close() 344 var qw quad.Writer = qwc 345 if len(format.Mime) != 0 { 346 w.Header().Set(hdrContentType, format.Mime[0]) 347 } 348 if irif := r.FormValue("iri"); irif != "" { 349 opts := quad.IRIOptions{ 350 Format: quad.IRIDefault, 351 } 352 switch irif { 353 case "short": 354 opts.Format = quad.IRIShort 355 case "full": 356 opts.Format = quad.IRIFull 357 } 358 qw = quad.IRIWriter(qw, opts) 359 } 360 if bw, ok := qw.(quad.BatchWriter); ok { 361 _, err = quad.CopyBatch(bw, qr, api.batch) 362 } else { 363 _, err = quad.Copy(qw, qr) 364 } 365 if err != nil && !cw.written { 366 jsonResponse(w, http.StatusInternalServerError, err) 367 return 368 } else if err != nil { 369 // can do nothing here, since first byte (and header) was written 370 // TODO: check if client just gone away 371 clog.Errorf("read quads error: %v", err) 372 } 373 } 374 375 func (api *APIv2) ServeFormats(w http.ResponseWriter, r *http.Request) { 376 type Format struct { 377 Id string `json:"id"` 378 Read bool `json:"read,omitempty"` 379 Write bool `json:"write,omitempty"` 380 Nodes bool `json:"nodes,omitempty"` 381 Ext []string `json:"ext,omitempty"` 382 Mime []string `json:"mime,omitempty"` 383 Binary bool `json:"binary,omitempty"` 384 } 385 formats := quad.Formats() 386 out := make([]Format, 0, len(formats)) 387 for _, f := range formats { 388 out = append(out, Format{ 389 Id: f.Name, 390 Ext: f.Ext, Mime: f.Mime, 391 Read: f.Reader != nil, Write: f.Writer != nil, 392 Nodes: f.UnmarshalValue != nil, 393 Binary: f.Binary, 394 }) 395 } 396 w.Header().Set(hdrContentType, contentTypeJSON) 397 json.NewEncoder(w).Encode(out) 398 } 399 400 func (api *APIv2) queryContext(r *http.Request) (ctx context.Context, cancel func()) { 401 ctx = r.Context() 402 if api.timeout > 0 { 403 ctx, cancel = context.WithTimeout(ctx, api.timeout) 404 } else { 405 ctx, cancel = context.WithCancel(ctx) 406 } 407 return ctx, cancel 408 } 409 410 func defaultErrorFunc(w query.ResponseWriter, err error) { 411 data, _ := json.Marshal(err.Error()) 412 w.WriteHeader(http.StatusBadRequest) 413 w.Write([]byte(`{"error": `)) 414 w.Write(data) 415 w.Write([]byte("}\n")) 416 } 417 418 func writeResults(w io.Writer, r interface{}) { 419 enc := json.NewEncoder(w) 420 enc.SetEscapeHTML(false) 421 enc.Encode(map[string]interface{}{ 422 "result": r, 423 }) 424 } 425 426 const maxQuerySize = 1024 * 1024 // 1 MB 427 func readLimit(r io.Reader) ([]byte, error) { 428 lr := io.LimitReader(r, maxQuerySize).(*io.LimitedReader) 429 data, err := ioutil.ReadAll(lr) 430 if err != nil && lr.N <= 0 { 431 err = errors.New("request is too large") 432 } 433 return data, err 434 } 435 436 func (api *APIv2) ServeQuery(w http.ResponseWriter, r *http.Request) { 437 ctx, cancel := api.queryContext(r) 438 defer cancel() 439 vals := r.URL.Query() 440 lang := vals.Get("lang") 441 if lang == "" { 442 jsonResponse(w, http.StatusBadRequest, "query language not specified") 443 return 444 } 445 l := query.GetLanguage(lang) 446 if l == nil { 447 jsonResponse(w, http.StatusBadRequest, "unknown query language") 448 return 449 } 450 errFunc := defaultErrorFunc 451 if l.HTTPError != nil { 452 errFunc = l.HTTPError 453 } 454 select { 455 case <-ctx.Done(): 456 errFunc(w, ctx.Err()) 457 return 458 default: 459 } 460 h, err := api.handleForRequest(r) 461 if err != nil { 462 errFunc(w, err) 463 return 464 } 465 if l.HTTPQuery != nil { 466 defer r.Body.Close() 467 l.HTTPQuery(ctx, h.QuadStore, w, r.Body) 468 return 469 } 470 if l.HTTP == nil { 471 errFunc(w, errors.New("HTTP interface is not supported for this query language")) 472 return 473 } 474 ses := l.HTTP(h.QuadStore) 475 var qu string 476 if r.Method == "GET" { 477 qu = vals.Get("qu") 478 } else { 479 data, err := readLimit(r.Body) 480 if err != nil { 481 errFunc(w, err) 482 return 483 } 484 qu = string(data) 485 } 486 if qu == "" { 487 jsonResponse(w, http.StatusBadRequest, "query is empty") 488 return 489 } 490 if clog.V(1) { 491 clog.Infof("query: %s: %q", lang, qu) 492 } 493 494 opt := query.Options{ 495 Collation: query.JSON, // TODO: switch to JSON-LD by default when the time comes 496 Limit: api.limit, 497 } 498 if specs := ParseAccept(r.Header, hdrAccept); len(specs) != 0 { 499 // TODO: sort by Q 500 switch specs[0].Value { 501 case contentTypeJSON: 502 opt.Collation = query.JSON 503 case contentTypeJSONLD: 504 opt.Collation = query.JSONLD 505 } 506 } 507 it, err := ses.Execute(ctx, qu, opt) 508 if err != nil { 509 errFunc(w, err) 510 return 511 } 512 defer it.Close() 513 514 var out []interface{} 515 for it.Next(ctx) { 516 out = append(out, it.Result()) 517 } 518 if err = it.Err(); err != nil { 519 errFunc(w, err) 520 return 521 } 522 if opt.Collation == query.JSONLD { 523 w.Header().Set(hdrContentType, contentTypeJSONLD) 524 } else { 525 w.Header().Set(hdrContentType, contentTypeJSON) 526 } 527 writeResults(w, out) 528 }