170 lines
4.2 KiB
Go
170 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type AnzoGraphClient struct {
|
|
cfg Config
|
|
endpoint string
|
|
authHeader string
|
|
client *http.Client
|
|
}
|
|
|
|
func NewAnzoGraphClient(cfg Config) *AnzoGraphClient {
|
|
endpoint := cfg.EffectiveSparqlEndpoint()
|
|
authHeader := ""
|
|
user := strings.TrimSpace(cfg.SparqlUser)
|
|
pass := strings.TrimSpace(cfg.SparqlPass)
|
|
if user != "" && pass != "" {
|
|
token := base64.StdEncoding.EncodeToString([]byte(user + ":" + pass))
|
|
authHeader = "Basic " + token
|
|
}
|
|
|
|
return &AnzoGraphClient{
|
|
cfg: cfg,
|
|
endpoint: endpoint,
|
|
authHeader: authHeader,
|
|
client: &http.Client{},
|
|
}
|
|
}
|
|
|
|
func (c *AnzoGraphClient) Startup(ctx context.Context) error {
|
|
if err := c.waitReady(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.cfg.SparqlClearOnStart {
|
|
if err := c.update(ctx, "CLEAR ALL"); err != nil {
|
|
return err
|
|
}
|
|
if err := c.waitReady(ctx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
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) {
|
|
ctx2, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
|
|
form := url.Values{}
|
|
form.Set("query", query)
|
|
|
|
req, err := http.NewRequestWithContext(ctx2, http.MethodPost, c.endpoint, strings.NewReader(form.Encode()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Set("Accept", "application/sparql-results+json")
|
|
if c.authHeader != "" {
|
|
req.Header.Set("Authorization", c.authHeader)
|
|
}
|
|
|
|
resp, err := c.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return nil, fmt.Errorf("sparql query failed: %s: %s", resp.Status, strings.TrimSpace(string(body)))
|
|
}
|
|
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")
|
|
if c.authHeader != "" {
|
|
req.Header.Set("Authorization", c.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
|
|
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:
|
|
}
|
|
|
|
body, err := c.queryWithTimeout(ctx, "ASK WHERE { ?s ?p ?o }", c.cfg.SparqlReadyTimeout)
|
|
if err == nil {
|
|
// Ensure it's JSON, not HTML/text during boot.
|
|
if strings.HasPrefix(strings.TrimSpace(string(body)), "{") {
|
|
return nil
|
|
}
|
|
err = fmt.Errorf("unexpected readiness response: %s", strings.TrimSpace(string(body)))
|
|
}
|
|
lastErr = err
|
|
time.Sleep(c.cfg.SparqlReadyDelay)
|
|
}
|
|
return fmt.Errorf("anzograph not ready at %s: %w", c.endpoint, lastErr)
|
|
}
|