Files
visualizador_instanciados/backend_go/server.go
2026-03-10 17:21:47 -03:00

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
}