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  }