| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577 | /* * EliasDB * * Copyright 2016 Matthias Ladkau. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */package v1import (	"encoding/json"	"fmt"	"net/http"	"strings"	"time"	"devt.de/krotik/common/datautil"	"devt.de/krotik/common/stringutil"	"devt.de/krotik/eliasdb/api"	"devt.de/krotik/eliasdb/eql"	"devt.de/krotik/eliasdb/graph/data")/*ResultCacheMaxSize is the maximum size for the result cache*/var ResultCacheMaxSize uint64/*ResultCacheMaxAge is the maximum age a result cache entry can have in seconds*/var ResultCacheMaxAge int64/*ResultCache is a cache for result sets (by default no expiry and no limit)*/var ResultCache *datautil.MapCache/*idCount is an ID counter for results*/var idCount = time.Now().Unix()/*EndpointQuery is the query endpoint URL (rooted). Handles everything under query/...*/const EndpointQuery = api.APIRoot + APIv1 + "/query/"/*QueryEndpointInst creates a new endpoint handler.*/func QueryEndpointInst() api.RestEndpointHandler {	// Init the result cache if necessary	if ResultCache == nil {		ResultCache = datautil.NewMapCache(ResultCacheMaxSize, ResultCacheMaxAge)	}	return &queryEndpoint{}}/*Handler object for search queries.*/type queryEndpoint struct {	*api.DefaultEndpointHandler}/*HandleGET handles a search query REST call.*/func (eq *queryEndpoint) HandleGET(w http.ResponseWriter, r *http.Request, resources []string) {	var err error	// Check parameters	if !checkResources(w, resources, 1, 1, "Need a partition") {		return	}	// Get partition	part := resources[0]	// Get limit parameter; -1 if not set	limit, ok := queryParamPosNum(w, r, "limit")	if !ok {		return	}	// Get offset parameter; -1 if not set	offset, ok := queryParamPosNum(w, r, "offset")	if !ok {		return	}	// Get groups parameter	gs := r.URL.Query().Get("groups")	showGroups := gs != ""	// See if a result ID was given	resID := r.URL.Query().Get("rid")	if resID != "" {		res, ok := ResultCache.Get(resID)		if !ok {			http.Error(w, "Unknown result ID (rid parameter)", http.StatusBadRequest)			return		}		err = eq.writeResultData(w, res.(*APISearchResult), part, resID, offset, limit, showGroups)	} else {		var res eql.SearchResult		// Run the query		query := r.URL.Query().Get("q")		if query == "" {			http.Error(w, "Missing query (q parameter)", http.StatusBadRequest)			return		}		res, err = eql.RunQuery(stringutil.CreateDisplayString(part)+" query",			part, query, api.GM)		if err == nil {			sres := &APISearchResult{res, nil}			// Make sure the result has a primary node column			_, err = sres.GetPrimaryNodeColumn()			if err != nil {				http.Error(w, err.Error(), http.StatusBadRequest)				return			}			// Store the result in the cache			resID = genID()			ResultCache.Put(resID, sres)			err = eq.writeResultData(w, sres, part, resID, offset, limit, showGroups)		}	}	if err != nil {		http.Error(w, err.Error(), http.StatusInternalServerError)	}}/*writeResultData writes result data for the client.*/func (eq *queryEndpoint) writeResultData(w http.ResponseWriter, res *APISearchResult,	part string, resID string, offset int, limit int, showGroups bool) error {	var err error	// Write out the data	header := res.Header()	ret := json.NewEncoder(w)	resdata := make(map[string]interface{})	// Count total selections	sels := res.Selections()	totalSels := 0	for _, s := range sels {		if s {			totalSels++		}	}	resdata["total_selections"] = totalSels	rows := res.Rows()	srcs := res.RowSources()	if limit == -1 && offset == -1 {		resdata["rows"] = rows		resdata["sources"] = srcs		resdata["selections"] = sels	} else {		if offset > 0 {			if offset >= len(rows) {				return fmt.Errorf("Offset exceeds available rows")			}			rows = rows[offset:]			srcs = srcs[offset:]			sels = sels[offset:]		}		if limit != -1 && limit < len(rows) {			rows = rows[:limit]			srcs = srcs[:limit]			sels = sels[:limit]		}		resdata["rows"] = rows		resdata["sources"] = srcs		resdata["selections"] = sels	}	// Write out result header	resdataHeader := make(map[string]interface{})	resdata["header"] = resdataHeader	resdataHeader["labels"] = header.Labels()	resdataHeader["format"] = header.Format()	resdataHeader["data"] = header.Data()	pk := header.PrimaryKind()	resdataHeader["primary_kind"] = pk	if showGroups {		groupList := make([][]string, 0, len(srcs))		if len(srcs) > 0 {			var col int			// Get column for primary kind			col, err = res.GetPrimaryNodeColumn()			// Lookup groups for nodes			for _, s := range resdata["sources"].([][]string) {				if err == nil {					var nodes []data.Node					groups := make([]string, 0, 3)					key := strings.Split(s[col], ":")[2]					nodes, _, err = api.GM.TraverseMulti(part, key, pk,						":::"+eql.GroupNodeKind, false)					if err == nil {						for _, n := range nodes {							groups = append(groups, n.Key())						}					}					groupList = append(groupList, groups)				}			}		}		resdata["groups"] = groupList	}	if err == nil {		// Set response header values		w.Header().Add(HTTPHeaderTotalCount, fmt.Sprint(res.RowCount()))		w.Header().Add(HTTPHeaderCacheID, resID)		w.Header().Set("content-type", "application/json; charset=utf-8")		ret.Encode(resdata)	}	return err}/*SwaggerDefs is used to describe the endpoint in swagger.*/func (eq *queryEndpoint) SwaggerDefs(s map[string]interface{}) {	// Add query paths	s["paths"].(map[string]interface{})["/v1/query/{partition}"] = map[string]interface{}{		"get": map[string]interface{}{			"summary": "Run EQL queries to query the EliasDB datastore.",			"description": "The query endpoint should be used to run EQL search " +				"queries against partitions. The return value is always a list " +				"(even if there is only a single entry). A query result gets an " +				"ID and is stored in a cache. The ID is returned in the X-Cache-Id " +				"header. Subsequent requests for the same result can use the ID instead of a query.",			"produces": []string{				"text/plain",				"application/json",			},			"parameters": []map[string]interface{}{				{					"name":        "partition",					"in":          "path",					"description": "Partition to query.",					"required":    true,					"type":        "string",				},				{					"name":        "q",					"in":          "query",					"description": "URL encoded query to execute.",					"required":    false,					"type":        "string",				},				{					"name":        "rid",					"in":          "query",					"description": "Result ID to retrieve from the result cache.",					"required":    false,					"type":        "number",					"format":      "integer",				},				{					"name":        "limit",					"in":          "query",					"description": "How many list items to return.",					"required":    false,					"type":        "number",					"format":      "integer",				},				{					"name":        "offset",					"in":          "query",					"description": "Offset in the dataset.",					"required":    false,					"type":        "number",					"format":      "integer",				},				{					"name":        "groups",					"in":          "query",					"description": "Include group information in the result if set to any value.",					"required":    false,					"type":        "number",					"format":      "integer",				},			},			"responses": map[string]interface{}{				"200": map[string]interface{}{					"description": "A query result",					"schema": map[string]interface{}{						"$ref": "#/definitions/QueryResult",					},				},				"default": map[string]interface{}{					"description": "Error response",					"schema": map[string]interface{}{						"$ref": "#/definitions/Error",					},				},			},		},	}	// Add QueryResult to definitions	s["definitions"].(map[string]interface{})["QueryResult"] = map[string]interface{}{		"type": "object",		"properties": map[string]interface{}{			"header": map[string]interface{}{				"description": "Header for the query result.",				"type":        "object",				"properties": map[string]interface{}{					"labels": map[string]interface{}{						"description": "All column labels of the search result.",						"type":        "array",						"items": map[string]interface{}{							"description": "Column label.",							"type":        "string",						},					},					"format": map[string]interface{}{						"description": "All column format definitions of the search result.",						"type":        "array",						"items": map[string]interface{}{							"description": "Column format as specified in the show format (e.g. text).",							"type":        "string",						},					},					"data": map[string]interface{}{						"description": "The data which is displayed in each column of the search result.",						"type":        "array",						"items": map[string]interface{}{							"description": "Data source for the column (e.g. 1:n:name - Name of starting nodes, 3:e:key - Key of edge traversed in the second traversal).",							"type":        "string",						},					},				},			},			"rows": map[string]interface{}{				"description": "Rows of the query result.",				"type":        "array",				"items": map[string]interface{}{					"description": "Columns of a row of the query result.",					"type":        "array",					"items": map[string]interface{}{						"description": "A single cell of the query result (string, integer or null).",						"type":        "object",					},				},			},			"sources": map[string]interface{}{				"description": "Data sources of the query result.",				"type":        "array",				"items": map[string]interface{}{					"description": "Columns of a row of the query result.",					"type":        "array",					"items": map[string]interface{}{						"description": "Data source of a single cell of the query result.",						"type":        "string",					},				},			},			"groups": map[string]interface{}{				"description": "Group names for each row.",				"type":        "array",				"items": map[string]interface{}{					"description": " Groups of the primary kind node.",					"type":        "array",					"items": map[string]interface{}{						"description": "Group name.",						"type":        "string",					},				},			},			"selections": map[string]interface{}{				"description": "List of row selections.",				"type":        "array",				"items": map[string]interface{}{					"description": "Row selection.",					"type":        "boolean",				},			},			"total_selections": map[string]interface{}{				"description": "Number of total selections.",				"type":        "number",				"format":      "integer",			},		},	}	// Add generic error object to definition	s["definitions"].(map[string]interface{})["Error"] = map[string]interface{}{		"description": "A human readable error mesage.",		"type":        "string",	}}/*genID generates a unique ID.*/func genID() string {	idCount++	return fmt.Sprint(idCount)}/*APISearchResult is a search result maintained by the API. It embeds*/type APISearchResult struct {	eql.SearchResult        // Normal eql search result	selections       []bool // Selections of the result}/*GetPrimaryNodeColumn determines the first primary node column.*/func (r *APISearchResult) GetPrimaryNodeColumn() (int, error) {	var err error	pk := r.Header().PrimaryKind()	col := -1	rs := r.RowSources()	if len(rs) > 0 {		for i, scol := range rs[0] {			scolParts := strings.Split(scol, ":")			if len(scolParts) > 1 && pk == scolParts[1] {				col = i			}		}	}	if col == -1 {		err = fmt.Errorf("Could not determine key of primary node - query needs a primary expression")	}	return col, err}/*Selections returns all current selections.*/func (r *APISearchResult) Selections() []bool {	r.refreshSelection()	return r.selections}/*SetSelection sets a new selection.*/func (r *APISearchResult) SetSelection(line int, selection bool) {	r.refreshSelection()	if line < len(r.selections) {		r.selections[line] = selection	}}/*AllSelection selects all rows.*/func (r *APISearchResult) AllSelection() {	r.refreshSelection()	for i := 0; i < len(r.selections); i++ {		r.selections[i] = true	}}/*NoneSelection selects none rows.*/func (r *APISearchResult) NoneSelection() {	r.refreshSelection()	for i := 0; i < len(r.selections); i++ {		r.selections[i] = false	}}/*InvertSelection inverts the current selection.*/func (r *APISearchResult) InvertSelection() {	r.refreshSelection()	for i := 0; i < len(r.selections); i++ {		r.selections[i] = !r.selections[i]	}}/*refreshSelection reallocates the selection array if necessary.*/func (r *APISearchResult) refreshSelection() {	l := r.SearchResult.RowCount()	if len(r.selections) != l {		origSelections := r.selections		// There is a difference between the selections array and the row		// count we need to resize		r.selections = make([]bool, l)		for i, s := range origSelections {			if i < l {				r.selections[i] = s			}		}	}}
 |