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 }