github.com/cayleygraph/cayley@v0.7.7/internal/gephi/stream.go (about)

     1  package gephi
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"math/rand"
    10  	"net/http"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/julienschmidt/httprouter"
    15  
    16  	"github.com/cayleygraph/cayley/clog"
    17  	"github.com/cayleygraph/cayley/graph"
    18  	"github.com/cayleygraph/cayley/graph/iterator"
    19  	"github.com/cayleygraph/cayley/graph/path"
    20  	"github.com/cayleygraph/cayley/graph/shape"
    21  	"github.com/cayleygraph/quad"
    22  	"github.com/cayleygraph/quad/voc/rdf"
    23  	"github.com/cayleygraph/quad/voc/rdfs"
    24  	"github.com/cayleygraph/quad/voc/schema"
    25  )
    26  
    27  const (
    28  	defaultLimit = 10000
    29  	defaultSize  = 20
    30  	limitCoord   = 500
    31  )
    32  
    33  const (
    34  	iriInlinePred = quad.IRI("gephi:inline")
    35  	iriPosX       = quad.IRI("gephi:x")
    36  	iriPosY       = quad.IRI("gephi:y")
    37  )
    38  
    39  var defaultInline = []quad.IRI{
    40  	iriPosX, iriPosY,
    41  
    42  	rdf.Type,
    43  	rdfs.Label,
    44  	schema.Name,
    45  	schema.UrlProp,
    46  }
    47  
    48  type GraphStreamHandler struct {
    49  	QS graph.QuadStore
    50  }
    51  
    52  type valHash [quad.HashSize]byte
    53  
    54  type GraphStream struct {
    55  	seen map[valHash]int
    56  	buf  *bytes.Buffer
    57  	w    io.Writer
    58  }
    59  
    60  func printNodeID(id int) string {
    61  	return strconv.FormatInt(int64(id), 16)
    62  }
    63  
    64  func NewGraphStream(w io.Writer) *GraphStream {
    65  	return &GraphStream{
    66  		w:    w,
    67  		seen: make(map[valHash]int),
    68  		buf:  bytes.NewBuffer(nil),
    69  	}
    70  }
    71  func toNodeLabel(v quad.Value) string {
    72  	if v == nil {
    73  		return ""
    74  	}
    75  	return fmt.Sprint(v.Native())
    76  }
    77  
    78  func randCoord() float64 {
    79  	return (rand.Float64() - 0.5) * limitCoord * 2
    80  }
    81  func randPos() (x float64, y float64) {
    82  	x = randCoord()
    83  	x2 := x * x
    84  	for y = randCoord(); x2+y*y > limitCoord*limitCoord; y = randCoord() {
    85  	}
    86  	return
    87  }
    88  
    89  func setStringProp(v *string, props map[quad.Value]quad.Value, name quad.IRI) {
    90  	if p, ok := props[name]; ok {
    91  		if s, ok := p.Native().(string); ok {
    92  			*v = s
    93  		}
    94  	}
    95  }
    96  
    97  func (gs *GraphStream) makeOneNode(id string, v quad.Value, props map[quad.Value]quad.Value) map[string]streamNode {
    98  	x, y := randPos()
    99  	var xok, yok bool
   100  	if p, ok := props[iriPosX]; ok {
   101  		xok = true
   102  		switch p := p.(type) {
   103  		case quad.Int:
   104  			x = float64(p)
   105  		case quad.Float:
   106  			x = float64(p)
   107  		default:
   108  			xok = false
   109  		}
   110  	}
   111  	if p, ok := props[iriPosY]; ok {
   112  		yok = true
   113  		switch p := p.(type) {
   114  		case quad.Int:
   115  			y = float64(p)
   116  		case quad.Float:
   117  			y = float64(p)
   118  		default:
   119  			yok = false
   120  		}
   121  	}
   122  	var slabel string
   123  	setStringProp(&slabel, props, rdfs.Label)
   124  	setStringProp(&slabel, props, schema.Name)
   125  
   126  	var label interface{}
   127  	if slabel != "" {
   128  		label = slabel
   129  	} else {
   130  		label = v.Native()
   131  	}
   132  
   133  	node := streamNode{
   134  		"label": label,
   135  		"size":  defaultSize, "x": x, "y": y,
   136  	}
   137  	for k, v := range props {
   138  		if k == nil || v == nil ||
   139  			(k == iriPosX && xok) ||
   140  			(k == iriPosY && yok) {
   141  			continue
   142  		}
   143  		node[toNodeLabel(k)] = toNodeLabel(v)
   144  	}
   145  	return map[string]streamNode{id: node}
   146  }
   147  func (gs *GraphStream) AddNode(v quad.Value, props map[quad.Value]quad.Value) string {
   148  	var h valHash
   149  	quad.HashTo(v, h[:])
   150  	return gs.addNode(v, h, props)
   151  }
   152  func (gs *GraphStream) encode(o interface{}) {
   153  	data, _ := json.Marshal(o)
   154  	gs.buf.Write(data)
   155  	// Gephi requires \r character at the end of each line
   156  	gs.buf.WriteString("\r\n")
   157  }
   158  func (gs *GraphStream) addNode(v quad.Value, h valHash, props map[quad.Value]quad.Value) string {
   159  	id, ok := gs.seen[h]
   160  	if ok {
   161  		return printNodeID(id)
   162  	} else if v == nil {
   163  		return ""
   164  	}
   165  	id = len(gs.seen)
   166  	gs.seen[h] = id
   167  	sid := printNodeID(id)
   168  
   169  	m := gs.makeOneNode(sid, v, props)
   170  	gs.encode(graphStreamEvent{AddNodes: m})
   171  	return sid
   172  }
   173  func (gs *GraphStream) ChangeNode(v quad.Value, sid string, props map[quad.Value]quad.Value) {
   174  	m := gs.makeOneNode(sid, v, props)
   175  	gs.encode(graphStreamEvent{ChangeNodes: m})
   176  }
   177  func (gs *GraphStream) AddEdge(i int, s, o string, p quad.Value) {
   178  	id := "q" + strconv.FormatInt(int64(i), 16)
   179  	ps := toNodeLabel(p)
   180  	gs.encode(graphStreamEvent{
   181  		AddEdges: map[string]streamEdge{id: {
   182  			Subject:   s,
   183  			Predicate: ps, Label: ps,
   184  			Object: o,
   185  		}},
   186  	})
   187  }
   188  func (gs *GraphStream) Flush() error {
   189  	if gs.buf.Len() == 0 {
   190  		return nil
   191  	}
   192  	_, err := gs.buf.WriteTo(gs.w)
   193  	if err == nil {
   194  		gs.buf.Reset()
   195  	}
   196  	return err
   197  }
   198  
   199  type streamNode map[string]interface{}
   200  type streamEdge struct {
   201  	Subject   string `json:"source"`
   202  	Label     string `json:"label"`
   203  	Predicate string `json:"pred"`
   204  	Object    string `json:"target"`
   205  }
   206  type graphStreamEvent struct {
   207  	AddNodes    map[string]streamNode `json:"an,omitempty"`
   208  	ChangeNodes map[string]streamNode `json:"cn,omitempty"`
   209  	DelNodes    map[string]streamNode `json:"dn,omitempty"`
   210  
   211  	AddEdges    map[string]streamEdge `json:"ae,omitempty"`
   212  	ChangeEdges map[string]streamEdge `json:"ce,omitempty"`
   213  	DelEdges    map[string]streamEdge `json:"de,omitempty"`
   214  }
   215  
   216  func (s *GraphStreamHandler) serveRawQuads(ctx context.Context, gs *GraphStream, quads shape.Shape, limit int) {
   217  	it := shape.BuildIterator(s.QS, quads)
   218  	defer it.Close()
   219  
   220  	var sh, oh valHash
   221  	for i := 0; (limit < 0 || i < limit) && it.Next(ctx); i++ {
   222  		qv := it.Result()
   223  		if qv == nil {
   224  			continue
   225  		}
   226  		q := s.QS.Quad(qv)
   227  		quad.HashTo(q.Subject, sh[:])
   228  		quad.HashTo(q.Object, oh[:])
   229  		s, o := gs.addNode(q.Subject, sh, nil), gs.addNode(q.Object, oh, nil)
   230  		if s == "" || o == "" {
   231  			continue
   232  		}
   233  		gs.AddEdge(i, s, o, q.Predicate)
   234  		if err := gs.Flush(); err != nil {
   235  			return
   236  		}
   237  	}
   238  }
   239  
   240  func shouldInline(v quad.Value) bool {
   241  	switch v.(type) {
   242  	case quad.Bool, quad.Int, quad.Float:
   243  		return true
   244  	}
   245  	return false
   246  }
   247  
   248  func (s *GraphStreamHandler) serveNodesWithProps(ctx context.Context, gs *GraphStream, limit int) {
   249  	propsPath := path.NewPath(s.QS).Has(iriInlinePred, quad.Bool(true))
   250  
   251  	// list of predicates marked as inline properties for gephi
   252  	inline := make(map[quad.Value]struct{})
   253  	err := propsPath.Iterate(ctx).EachValue(s.QS, func(v quad.Value) {
   254  		inline[v] = struct{}{}
   255  	})
   256  	if err != nil {
   257  		clog.Errorf("cannot iterate over properties: %v", err)
   258  		return
   259  	}
   260  	// inline some well-known predicates
   261  	for _, iri := range defaultInline {
   262  		inline[iri] = struct{}{}
   263  		inline[iri.Full()] = struct{}{}
   264  	}
   265  
   266  	ignore := make(map[quad.Value]struct{})
   267  
   268  	nodes := iterator.NewNot(propsPath.BuildIterator(), s.QS.NodesAllIterator())
   269  	defer nodes.Close()
   270  
   271  	ictx, cancel := context.WithCancel(ctx)
   272  	defer cancel()
   273  
   274  	itc := graph.Iterate(ictx, nodes).On(s.QS).Limit(limit)
   275  
   276  	qi := 0
   277  	_ = itc.EachValuePair(s.QS, func(v graph.Ref, nv quad.Value) {
   278  		if _, skip := ignore[nv]; skip {
   279  			return
   280  		}
   281  		// list of inline properties
   282  		props := make(map[quad.Value]quad.Value)
   283  
   284  		var (
   285  			sid   string
   286  			h, oh valHash
   287  		)
   288  		quad.HashTo(nv, h[:])
   289  
   290  		predIt := s.QS.QuadIterator(quad.Subject, nodes.Result())
   291  		defer predIt.Close()
   292  		for predIt.Next(ictx) {
   293  			// this check helps us ignore nodes with no links
   294  			if sid == "" {
   295  				sid = gs.addNode(nv, h, props)
   296  			}
   297  			q := s.QS.Quad(predIt.Result())
   298  			if _, ok := inline[q.Predicate]; ok {
   299  				props[q.Predicate] = q.Object
   300  				ignore[q.Object] = struct{}{}
   301  			} else if shouldInline(q.Object) {
   302  				props[q.Predicate] = q.Object
   303  			} else {
   304  				quad.HashTo(q.Object, oh[:])
   305  				o := gs.addNode(q.Object, oh, nil)
   306  				if o == "" {
   307  					continue
   308  				}
   309  				gs.AddEdge(qi, sid, o, q.Predicate)
   310  				qi++
   311  				if err := gs.Flush(); err != nil {
   312  					cancel()
   313  					return
   314  				}
   315  			}
   316  		}
   317  		if err := predIt.Err(); err != nil {
   318  			cancel()
   319  			return
   320  		} else if sid == "" {
   321  			return
   322  		}
   323  		if len(props) != 0 {
   324  			gs.ChangeNode(nv, sid, props)
   325  		}
   326  		if err := gs.Flush(); err != nil {
   327  			cancel()
   328  			return
   329  		}
   330  	})
   331  }
   332  
   333  func valuesFromString(s string) []quad.Value {
   334  	if s == "" {
   335  		return nil
   336  	}
   337  	arr := strings.Split(s, ",")
   338  	out := make([]quad.Value, 0, len(arr))
   339  	for _, s := range arr {
   340  		out = append(out, quad.StringToValue(s))
   341  	}
   342  	return out
   343  }
   344  
   345  func (s *GraphStreamHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
   346  	ctx := context.TODO()
   347  	var limit int
   348  	if s := r.FormValue("limit"); s != "" {
   349  		limit, _ = strconv.Atoi(s)
   350  	}
   351  	if limit == 0 {
   352  		limit = defaultLimit
   353  	}
   354  	mode := "raw"
   355  	if s := r.FormValue("mode"); s != "" {
   356  		mode = s
   357  	}
   358  
   359  	w.Header().Set("Content-Type", "application/stream+json")
   360  	gs := NewGraphStream(w)
   361  	switch mode {
   362  	case "nodes":
   363  		s.serveNodesWithProps(ctx, gs, limit)
   364  	case "raw":
   365  		values := shape.FilterQuads(
   366  			valuesFromString(r.FormValue("sub")),
   367  			valuesFromString(r.FormValue("pred")),
   368  			valuesFromString(r.FormValue("obj")),
   369  			valuesFromString(r.FormValue("label")),
   370  		)
   371  		s.serveRawQuads(ctx, gs, values, limit)
   372  	default:
   373  		w.WriteHeader(http.StatusBadRequest)
   374  		return
   375  	}
   376  }