package main import ( "context" "encoding/base64" "fmt" "io" "log" "net/http" "strings" "time" "visualizador_instanciados/backend_go/queryscope" ) type AnzoGraphClient struct { cfg Config endpoint string basicAuthHeader string client *http.Client tokenManager *keycloakTokenManager } func NewAnzoGraphClient(cfg Config) *AnzoGraphClient { endpoint := cfg.EffectiveSparqlEndpoint() client := &http.Client{} basicAuthHeader := "" if !cfg.UsesExternalSparql() { user := strings.TrimSpace(cfg.SparqlUser) pass := strings.TrimSpace(cfg.SparqlPass) if user != "" && pass != "" { token := base64.StdEncoding.EncodeToString([]byte(user + ":" + pass)) basicAuthHeader = "Basic " + token } } agc := &AnzoGraphClient{ cfg: cfg, endpoint: endpoint, basicAuthHeader: basicAuthHeader, client: client, } if cfg.UsesExternalSparql() { agc.tokenManager = newKeycloakTokenManager(cfg, client) } return agc } func (c *AnzoGraphClient) Startup(ctx context.Context) error { log.Printf( "[sparql] startup source_mode=%s endpoint=%s auth_mode=%s load_on_start=%t", c.cfg.SparqlSourceMode, c.endpoint, c.authMode(), c.cfg.SparqlLoadOnStart, ) if c.cfg.UsesExternalSparql() { tokenCtx, cancel := context.WithTimeout(ctx, c.cfg.SparqlReadyTimeout) defer cancel() if _, err := c.refreshExternalToken(tokenCtx, "startup"); err != nil { return fmt.Errorf("keycloak startup token fetch failed: %w", err) } } if err := c.waitReady(ctx); err != nil { return err } c.logNamedGraphDatasetProbe(ctx, "startup_initial") if c.cfg.SparqlLoadOnStart { df := strings.TrimSpace(c.cfg.SparqlDataFile) if df == "" { return fmt.Errorf("SPARQL_LOAD_ON_START=true but SPARQL_DATA_FILE is not set") } giri := strings.TrimSpace(c.cfg.SparqlGraphIRI) if giri != "" { if err := c.update(ctx, fmt.Sprintf("LOAD <%s> INTO GRAPH <%s>", df, giri)); err != nil { return err } } else { if err := c.update(ctx, fmt.Sprintf("LOAD <%s>", df)); err != nil { return err } } if err := c.waitReady(ctx); err != nil { return err } c.logNamedGraphDatasetProbe(ctx, "startup_post_load") } return nil } func (c *AnzoGraphClient) Shutdown(ctx context.Context) error { _ = ctx return nil } func (c *AnzoGraphClient) Query(ctx context.Context, query string) ([]byte, error) { return c.queryWithTimeout(ctx, query, c.cfg.SparqlTimeout) } func (c *AnzoGraphClient) queryWithTimeout(ctx context.Context, query string, timeout time.Duration) ([]byte, error) { resp, _, err := c.queryRequestWithTimeout(ctx, query, timeout) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return body, nil } func (c *AnzoGraphClient) update(ctx context.Context, update string) error { ctx2, cancel := context.WithTimeout(ctx, c.cfg.SparqlTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx2, http.MethodPost, c.endpoint, strings.NewReader(update)) if err != nil { return err } req.Header.Set("Content-Type", "application/sparql-update") req.Header.Set("Accept", "application/json") authHeader, err := c.authorizationHeader(ctx2, "sparql_update") if err != nil { return err } if authHeader != "" { req.Header.Set("Authorization", authHeader) } resp, err := c.client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("sparql update failed: %s: %s", resp.Status, strings.TrimSpace(string(body))) } return nil } func (c *AnzoGraphClient) waitReady(ctx context.Context) error { var lastErr error log.Printf( "[sparql] readiness_wait_start endpoint=%s retries=%d timeout=%s delay=%s query_scope=named_graphs", c.endpoint, c.cfg.SparqlReadyRetries, c.cfg.SparqlReadyTimeout, c.cfg.SparqlReadyDelay, ) for i := 0; i < c.cfg.SparqlReadyRetries; i++ { select { case <-ctx.Done(): if lastErr != nil { return fmt.Errorf("anzograph not ready at %s: %w", c.endpoint, lastErr) } return ctx.Err() default: } var ask sparqlBooleanResponse _, err := c.queryJSONWithTimeout(ctx, namedGraphAnyTripleAskQuery(), c.cfg.SparqlReadyTimeout, &ask) if err == nil { log.Printf("[sparql] readiness_wait_ok endpoint=%s attempt=%d/%d", c.endpoint, i+1, c.cfg.SparqlReadyRetries) return nil } lastErr = err log.Printf("[sparql] readiness_wait_retry endpoint=%s attempt=%d/%d err=%v", c.endpoint, i+1, c.cfg.SparqlReadyRetries, err) time.Sleep(c.cfg.SparqlReadyDelay) } return fmt.Errorf("anzograph not ready at %s: %w", c.endpoint, lastErr) } func namedGraphAnyTripleAskQuery() string { return queryscope.AskAnyTripleQuery() } func (c *AnzoGraphClient) authMode() string { switch { case c.cfg.UsesExternalSparql(): return "bearer" case strings.HasPrefix(c.basicAuthHeader, "Basic "): return "basic" default: return "none" } } func (c *AnzoGraphClient) authorizationHeader(ctx context.Context, reason string) (string, error) { if !c.cfg.UsesExternalSparql() { return c.basicAuthHeader, nil } if c.tokenManager == nil { return "", fmt.Errorf("external sparql mode is enabled but token manager is not configured") } token, err := c.tokenManager.EnsureToken(ctx, reason) if err != nil { return "", err } return "Bearer " + token, nil } func (c *AnzoGraphClient) refreshExternalToken(ctx context.Context, reason string) (string, error) { if !c.cfg.UsesExternalSparql() { return "", nil } if c.tokenManager == nil { return "", fmt.Errorf("external sparql mode is enabled but token manager is not configured") } return c.tokenManager.Refresh(ctx, reason) } func (c *AnzoGraphClient) logNamedGraphDatasetProbe(ctx context.Context, stage string) { var ask sparqlBooleanResponse metrics, err := c.queryJSONWithTimeout(ctx, namedGraphAnyTripleAskQuery(), c.cfg.SparqlReadyTimeout, &ask) if err != nil { log.Printf("[sparql] dataset_probe_failed stage=%s endpoint=%s err=%v", stage, c.endpoint, err) return } log.Printf( "[sparql] dataset_probe stage=%s endpoint=%s named_graph_has_triples=%t bytes=%d round_trip_time=%s decode_time=%s", stage, c.endpoint, ask.Boolean, metrics.ResponseBytes, metrics.RoundTripTime.Truncate(time.Millisecond), metrics.BodyDecodeTime.Truncate(time.Millisecond), ) }