305 lines
8.8 KiB
Go
305 lines
8.8 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
graphqueries "visualizador_instanciados/backend_go/graph_queries"
|
|
selectionqueries "visualizador_instanciados/backend_go/selection_queries"
|
|
)
|
|
|
|
type APIServer struct {
|
|
cfg Config
|
|
sparql *AnzoGraphClient
|
|
snapshots *GraphSnapshotService
|
|
}
|
|
|
|
func (s *APIServer) handler() http.Handler {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/health", s.handleHealth)
|
|
mux.HandleFunc("/api/stats", s.handleStats)
|
|
mux.HandleFunc("/api/sparql", s.handleSparql)
|
|
mux.HandleFunc("/api/graph", s.handleGraph)
|
|
mux.HandleFunc("/api/graph_queries", s.handleGraphQueries)
|
|
mux.HandleFunc("/api/selection_queries", s.handleSelectionQueries)
|
|
mux.HandleFunc("/api/selection_query", s.handleSelectionQuery)
|
|
mux.HandleFunc("/api/neighbors", s.handleNeighbors)
|
|
|
|
return s.corsMiddleware(mux)
|
|
}
|
|
|
|
func (s *APIServer) corsMiddleware(next http.Handler) http.Handler {
|
|
origins := s.cfg.corsOriginList()
|
|
allowAll := len(origins) == 1 && origins[0] == "*"
|
|
allowed := make(map[string]struct{}, len(origins))
|
|
for _, o := range origins {
|
|
allowed[o] = struct{}{}
|
|
}
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
origin := r.Header.Get("Origin")
|
|
if origin != "" {
|
|
if allowAll {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
} else if _, ok := allowed[origin]; ok {
|
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
|
w.Header().Add("Vary", "Origin")
|
|
}
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "*")
|
|
}
|
|
|
|
if r.Method == http.MethodOptions {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func (s *APIServer) handleHealth(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, HealthResponse{Status: "ok"})
|
|
}
|
|
|
|
func (s *APIServer) handleStats(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
ctx := r.Context()
|
|
snap, err := s.snapshots.Get(ctx, s.cfg.DefaultNodeLimit, s.cfg.DefaultEdgeLimit, graphqueries.DefaultID)
|
|
if err != nil {
|
|
log.Printf("handleStats: snapshot error: %v", err)
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
endpoint := snap.Meta.SparqlEndpoint
|
|
writeJSON(w, http.StatusOK, StatsResponse{
|
|
Backend: snap.Meta.Backend,
|
|
TTLPath: snap.Meta.TTLPath,
|
|
SparqlEndpoint: &endpoint,
|
|
ParsedTriples: len(snap.Edges),
|
|
Nodes: len(snap.Nodes),
|
|
Edges: len(snap.Edges),
|
|
})
|
|
}
|
|
|
|
func (s *APIServer) handleSparql(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var req SparqlQueryRequest
|
|
if err := decodeJSON(r.Body, &req); err != nil || strings.TrimSpace(req.Query) == "" {
|
|
writeError(w, http.StatusUnprocessableEntity, "invalid request body")
|
|
return
|
|
}
|
|
|
|
raw, err := s.sparql.Query(r.Context(), req.Query)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadGateway, err.Error())
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(raw)
|
|
}
|
|
|
|
func (s *APIServer) handleGraph(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
nodeLimit, err := intQuery(r, "node_limit", s.cfg.DefaultNodeLimit)
|
|
if err != nil || nodeLimit < 1 || nodeLimit > s.cfg.MaxNodeLimit {
|
|
writeError(w, http.StatusUnprocessableEntity, fmt.Sprintf("node_limit must be between 1 and %d", s.cfg.MaxNodeLimit))
|
|
return
|
|
}
|
|
edgeLimit, err := intQuery(r, "edge_limit", s.cfg.DefaultEdgeLimit)
|
|
if err != nil || edgeLimit < 1 || edgeLimit > s.cfg.MaxEdgeLimit {
|
|
writeError(w, http.StatusUnprocessableEntity, fmt.Sprintf("edge_limit must be between 1 and %d", s.cfg.MaxEdgeLimit))
|
|
return
|
|
}
|
|
|
|
graphQueryID := strings.TrimSpace(r.URL.Query().Get("graph_query_id"))
|
|
if graphQueryID == "" {
|
|
graphQueryID = graphqueries.DefaultID
|
|
}
|
|
if _, ok := graphqueries.Get(graphQueryID); !ok {
|
|
writeError(w, http.StatusUnprocessableEntity, "unknown graph_query_id")
|
|
return
|
|
}
|
|
|
|
snap, err := s.snapshots.Get(r.Context(), nodeLimit, edgeLimit, graphQueryID)
|
|
if err != nil {
|
|
log.Printf("handleGraph: snapshot error graph_query_id=%s node_limit=%d edge_limit=%d err=%v", graphQueryID, nodeLimit, edgeLimit, err)
|
|
if _, ok := err.(*CycleError); ok {
|
|
writeError(w, http.StatusUnprocessableEntity, err.Error())
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, snap)
|
|
}
|
|
|
|
func (s *APIServer) handleGraphQueries(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, graphqueries.List())
|
|
}
|
|
|
|
func (s *APIServer) handleSelectionQueries(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, selectionqueries.List())
|
|
}
|
|
|
|
func (s *APIServer) handleSelectionQuery(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var req SelectionQueryRequest
|
|
if err := decodeJSON(r.Body, &req); err != nil || strings.TrimSpace(req.QueryID) == "" {
|
|
writeError(w, http.StatusUnprocessableEntity, "invalid request body")
|
|
return
|
|
}
|
|
if _, ok := selectionqueries.Get(req.QueryID); !ok {
|
|
writeError(w, http.StatusUnprocessableEntity, "unknown query_id")
|
|
return
|
|
}
|
|
if len(req.SelectedIDs) == 0 {
|
|
writeJSON(w, http.StatusOK, SelectionQueryResponse{
|
|
QueryID: req.QueryID,
|
|
SelectedIDs: req.SelectedIDs,
|
|
NeighborIDs: []uint32{},
|
|
})
|
|
return
|
|
}
|
|
|
|
graphQueryID := graphqueries.DefaultID
|
|
if req.GraphQueryID != nil && strings.TrimSpace(*req.GraphQueryID) != "" {
|
|
graphQueryID = strings.TrimSpace(*req.GraphQueryID)
|
|
}
|
|
if _, ok := graphqueries.Get(graphQueryID); !ok {
|
|
writeError(w, http.StatusUnprocessableEntity, "unknown graph_query_id")
|
|
return
|
|
}
|
|
|
|
nodeLimit := s.cfg.DefaultNodeLimit
|
|
edgeLimit := s.cfg.DefaultEdgeLimit
|
|
if req.NodeLimit != nil {
|
|
nodeLimit = *req.NodeLimit
|
|
}
|
|
if req.EdgeLimit != nil {
|
|
edgeLimit = *req.EdgeLimit
|
|
}
|
|
if nodeLimit < 1 || nodeLimit > s.cfg.MaxNodeLimit || edgeLimit < 1 || edgeLimit > s.cfg.MaxEdgeLimit {
|
|
writeError(w, http.StatusUnprocessableEntity, "invalid node_limit/edge_limit")
|
|
return
|
|
}
|
|
|
|
snap, err := s.snapshots.Get(r.Context(), nodeLimit, edgeLimit, graphQueryID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
ids, err := runSelectionQuery(r.Context(), s.sparql, snap, req.QueryID, req.SelectedIDs, s.cfg.IncludeBNodes)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadGateway, err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, SelectionQueryResponse{
|
|
QueryID: req.QueryID,
|
|
SelectedIDs: req.SelectedIDs,
|
|
NeighborIDs: ids,
|
|
})
|
|
}
|
|
|
|
func (s *APIServer) handleNeighbors(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var req NeighborsRequest
|
|
if err := decodeJSON(r.Body, &req); err != nil {
|
|
writeError(w, http.StatusUnprocessableEntity, "invalid request body")
|
|
return
|
|
}
|
|
if len(req.SelectedIDs) == 0 {
|
|
writeJSON(w, http.StatusOK, NeighborsResponse{SelectedIDs: req.SelectedIDs, NeighborIDs: []uint32{}})
|
|
return
|
|
}
|
|
|
|
graphQueryID := graphqueries.DefaultID
|
|
if req.GraphQueryID != nil && strings.TrimSpace(*req.GraphQueryID) != "" {
|
|
graphQueryID = strings.TrimSpace(*req.GraphQueryID)
|
|
}
|
|
if _, ok := graphqueries.Get(graphQueryID); !ok {
|
|
writeError(w, http.StatusUnprocessableEntity, "unknown graph_query_id")
|
|
return
|
|
}
|
|
|
|
nodeLimit := s.cfg.DefaultNodeLimit
|
|
edgeLimit := s.cfg.DefaultEdgeLimit
|
|
if req.NodeLimit != nil {
|
|
nodeLimit = *req.NodeLimit
|
|
}
|
|
if req.EdgeLimit != nil {
|
|
edgeLimit = *req.EdgeLimit
|
|
}
|
|
if nodeLimit < 1 || nodeLimit > s.cfg.MaxNodeLimit || edgeLimit < 1 || edgeLimit > s.cfg.MaxEdgeLimit {
|
|
writeError(w, http.StatusUnprocessableEntity, "invalid node_limit/edge_limit")
|
|
return
|
|
}
|
|
|
|
snap, err := s.snapshots.Get(r.Context(), nodeLimit, edgeLimit, graphQueryID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
nbrs, err := runSelectionQuery(r.Context(), s.sparql, snap, "neighbors", req.SelectedIDs, s.cfg.IncludeBNodes)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadGateway, err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, NeighborsResponse{SelectedIDs: req.SelectedIDs, NeighborIDs: nbrs})
|
|
}
|
|
|
|
func intQuery(r *http.Request, name string, def int) (int, error) {
|
|
raw := strings.TrimSpace(r.URL.Query().Get(name))
|
|
if raw == "" {
|
|
return def, nil
|
|
}
|
|
n, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return n, nil
|
|
}
|