gitlab-ci: enable neo4j
[nit.git] / lib / neo4j / neo4j.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Neo4j connector through its JSON REST API using curl.
16 #
17 # In order to connect to Neo4j you need a connector:
18 #
19 # # Create new Neo4j client
20 # var client = new Neo4jClient("http://neo4j:7474")
21 # assert client.is_ok
22 #
23 # The fundamental units that form a graph are nodes and relationships.
24 #
25 # Nodes are used to represent entities stored in base:
26 #
27 # # Create a disconnected node
28 # var andres = new NeoNode
29 # andres["name"] = "Andres"
30 # # Connect the node to Neo4j
31 # client.save_node(andres)
32 # assert andres.is_linked
33 #
34 # # Create a second node
35 # var kate = new NeoNode
36 # kate["name"] = "Kate"
37 # client.save_node(kate)
38 # assert kate.is_linked
39 #
40 # Relationships between nodes are a key part of a graph database.
41 # They allow for finding related data. Just like nodes, relationships can have properties.
42 #
43 # # Create a relationship
44 # var loves = new NeoEdge(andres, "LOVES", kate)
45 # client.save_edge(loves)
46 # assert loves.is_linked
47 #
48 # Nodes can also be loaded fron Neo4j:
49 #
50 # # Get a node from DB and explore edges
51 # var url = andres.url.to_s
52 # var from = client.load_node(url)
53 # assert from["name"].to_s == "Andres"
54 # var to = from.out_nodes("LOVES").first # follow the first LOVES relationship
55 # assert to["name"].to_s == "Kate"
56 #
57 # For more details, see http://docs.neo4j.org/chunked/milestone/rest-api.html
58 module neo4j
59
60 import curl_json
61 import error
62
63 # `Neo4jClient` is needed to communicate through the REST API
64 #
65 # var client = new Neo4jClient("http://neo4j:7474")
66 # assert client.is_ok
67 class Neo4jClient
68
69 # Neo4j REST services baseurl
70 var base_url: String
71 # REST service to get node data
72 private var node_url: String
73 # REST service to batch
74 private var batch_url: String
75 # REST service to send cypher requests
76 private var cypher_url: String
77
78 private var curl = new Curl
79
80 init(base_url: String) do
81 self.base_url = base_url
82 var root = service_root
83 assert root isa JsonObject else
84 sys.stderr.write "Neo4jClientError: cannot connect to server at <{base_url}>.\n"
85 end
86 self.node_url = root["node"].to_s
87 self.batch_url = root["batch"].to_s
88 self.cypher_url = root["cypher"].to_s
89 end
90
91 fun service_root: Serializable do return get(base_url / "db/data")
92
93 # Is the connection with the Neo4j server ok?
94 fun is_ok: Bool do return service_root isa JsonObject
95
96 # Empty the graph
97 fun clear_graph do
98 cypher(new CypherQuery.from_string("MATCH (n) OPTIONAL MATCH n-[r]-() DELETE r, n"))
99 end
100
101 # Last errors
102 var errors = new Array[String]
103
104 # Nodes view stored locally
105 private var local_nodes = new HashMap[String, nullable NeoNode]
106
107 # Save the node in base
108 #
109 # var client = new Neo4jClient("http://neo4j:7474")
110 #
111 # # Create a node
112 # var andres = new NeoNode
113 # andres["name"] = "Andres"
114 # client.save_node(andres)
115 # assert andres.is_linked
116 #
117 # Once linked, nodes cannot be created twice:
118 #
119 # var oldurl = andres.url
120 # client.save_node(andres) # do nothing
121 # assert andres.url == oldurl
122 fun save_node(node: NeoNode): Bool do
123 if node.is_linked then return true
124 node.neo = self
125 var batch = new NeoBatch(self)
126 batch.save_node(node)
127 # batch.create_edges(node.out_edges)
128 var errors = batch.execute
129 if not errors.is_empty then
130 errors.add_all errors
131 return false
132 end
133 local_nodes[node.url.to_s] = node
134 return true
135 end
136
137 # Load a node from base
138 # Data, labels and edges will be loaded lazily.
139 fun load_node(url: String): NeoNode do
140 if local_nodes.has_key(url) then
141 var node = local_nodes[url]
142 if node != null then return node
143 end
144 var node = new NeoNode.from_neo(self, url)
145 local_nodes[url] = node
146 return node
147 end
148
149 # Remove the entity from base
150 fun delete_node(node: NeoNode): Bool do
151 if not node.is_linked then return false
152 var url = node.url.to_s
153 delete(url)
154 local_nodes[url] = null
155 node.url = null
156 return true
157 end
158
159 # Edges view stored locally
160 private var local_edges = new HashMap[String, nullable NeoEdge]
161
162 # Save the edge in base
163 # From and to nodes will be created.
164 #
165 # var client = new Neo4jClient("http://neo4j:7474")
166 #
167 # var andres = new NeoNode
168 # var kate = new NeoNode
169 # var edge = new NeoEdge(andres, "LOVES", kate)
170 # client.save_edge(edge)
171 # assert andres.is_linked
172 # assert kate.is_linked
173 # assert edge.is_linked
174 fun save_edge(edge: NeoEdge): Bool do
175 if edge.is_linked then return true
176 edge.neo = self
177 edge.from.out_edges.add edge
178 edge.to.in_edges.add edge
179 var batch = new NeoBatch(self)
180 batch.save_edge(edge)
181 var errors = batch.execute
182 if not errors.is_empty then
183 errors.add_all errors
184 return false
185 end
186 local_edges[edge.url.to_s] = edge
187 return true
188 end
189
190 # Load a edge from base
191 # Data will be loaded lazily.
192 fun load_edge(url: String): NeoEdge do
193 if local_edges.has_key(url) then
194 var node = local_edges[url]
195 if node != null then return node
196 end
197 var edge = new NeoEdge.from_neo(self, url)
198 local_edges[url] = edge
199 return edge
200 end
201
202 # Remove the edge from base
203 fun delete_edge(edge: NeoEdge): Bool do
204 if not edge.is_linked then return false
205 var url = edge.url.to_s
206 delete(url)
207 local_edges[url] = null
208 edge.url = null
209 return true
210 end
211
212 # Retrieve all nodes with specified `lbl`
213 #
214 # var client = new Neo4jClient("http://neo4j:7474")
215 #
216 # var andres = new NeoNode
217 # andres.labels.add_all(["Human", "Male"])
218 # client.save_node(andres)
219 # var kate = new NeoNode
220 # kate.labels.add_all(["Human", "Female"])
221 # client.save_node(kate)
222 #
223 # var nodes = client.nodes_with_label("Human")
224 # assert nodes.has(andres)
225 # assert nodes.has(kate)
226 fun nodes_with_label(lbl: String): Array[NeoNode] do
227 var res = get(base_url / "db/data/label/{lbl.to_percent_encoding}/nodes")
228 var nodes = new Array[NeoNode]
229 for json in res.as(JsonArray) do
230 var obj = json.as(JsonObject)
231 var node = load_node(obj["self"].to_s)
232 node.internal_properties = obj["data"].as(JsonObject)
233 nodes.add node
234 end
235 return nodes
236 end
237
238 # Retrieve nodes belonging to all the specified `labels`.
239 #
240 # var client = new Neo4jClient("http://neo4j:7474")
241 #
242 # var andres = new NeoNode
243 # andres.labels.add_all(["Human", "Male"])
244 # client.save_node(andres)
245 # var kate = new NeoNode
246 # kate.labels.add_all(["Human", "Female"])
247 # client.save_node(kate)
248 #
249 # var nodes = client.nodes_with_labels(["Human", "Male"])
250 # assert nodes.has(andres)
251 # assert not nodes.has(kate)
252 fun nodes_with_labels(labels: Array[String]): Array[NeoNode] do
253 assert not labels.is_empty
254
255 # Build the query.
256 var buffer = new Buffer
257 buffer.append "match (n) where \{label_0\} in labels(n)"
258 for i in [1..labels.length[ do
259 buffer.append " and \{label_{i}\} in labels(n)"
260 end
261 buffer.append " return n"
262 var query = new CypherQuery.from_string(buffer.write_to_string)
263 for i in [0..labels.length[ do
264 query.params["label_{i}"] = labels[i]
265 end
266
267 # Retrieve the answer.
268 var res = cypher(query)
269 var nodes = new Array[NeoNode]
270 for json in res.as(JsonObject)["data"].as(JsonArray) do
271 var obj = json.as(JsonArray).first.as(JsonObject)
272 var node = load_node(obj["self"].to_s)
273 node.internal_properties = obj["data"].as(JsonObject)
274 nodes.add node
275 end
276 return nodes
277 end
278
279 # Perform a `CypherQuery`
280 # see: CypherQuery
281 fun cypher(query: CypherQuery): Serializable do
282 return post("{cypher_url}", query.to_rest)
283 end
284
285 # GET JSON data from `url`
286 fun get(url: String): Serializable do
287 var request = new JsonGET(url)
288 var response = request.execute
289 return parse_response(response)
290 end
291
292 # POST `params` to `url`
293 fun post(url: String, params: Serializable): Serializable do
294 var request = new JsonPOST(url)
295 request.json_data = params
296 var response = request.execute
297 return parse_response(response)
298 end
299
300 # PUT `params` at `url`
301 fun put(url: String, params: Serializable): Serializable do
302 var request = new JsonPUT(url)
303 request.json_data = params
304 var response = request.execute
305 return parse_response(response)
306 end
307
308 # DELETE `url`
309 fun delete(url: String): Serializable do
310 var request = new JsonDELETE(url)
311 var response = request.execute
312 return parse_response(response)
313 end
314
315 # Parse the cURL `response` as a JSON string
316 private fun parse_response(response: CurlResponse): Serializable do
317 if response isa CurlResponseSuccess then
318 var str = response.body_str
319 if str.is_empty then return new JsonObject
320 var res = str.parse_json
321 if res isa JsonParseError then
322 var e = new NeoError(res.to_s, "JsonParseError")
323 e.cause = res
324 return e
325 end
326 if res == null then
327 # empty response wrap it in empty object
328 return new JsonObject
329 else if res isa JsonObject and res.has_key("exception") then
330 var error = "Neo4jError::{res["exception"] or else "null"}"
331 var msg = ""
332 if res.has_key("message") then
333 msg = res["message"].to_s
334 end
335 return new NeoError(msg, error)
336 else
337 return res
338 end
339 else if response isa CurlResponseFailed then
340 return new NeoError("{response.error_msg} ({response.error_code})", "CurlError")
341 else
342 return new NeoError("Unexpected response \"{response}\".", "CurlError")
343 end
344 end
345 end
346
347 # A Cypher query for Neo4j REST API
348 #
349 # The Neo4j REST API allows querying with Cypher.
350 # The results are returned as a list of string headers (columns), and a data part,
351 # consisting of a list of all rows, every row consisting of a list of REST representations
352 # of the field value - Node, Relationship, Path or any simple value like String.
353 #
354 # Example:
355 #
356 # var client = new Neo4jClient("http://neo4j:7474")
357 # var query = new CypherQuery
358 # query.nmatch("(n)-[r:LOVES]->(m)")
359 # query.nwhere("n.name=\"Andres\"")
360 # query.nreturn("m.name")
361 # var res = client.cypher(query).as(JsonObject)
362 # assert res["data"].as(JsonArray).first.as(JsonArray).first == "Kate"
363 #
364 # For more details, see: http://docs.neo4j.org/chunked/milestone/rest-api-cypher.html
365 class CypherQuery
366 # Query string to perform
367 private var query: String = ""
368
369 # `params` to embed in the query like in prepared statements
370 var params = new JsonObject
371
372 # init the query from a query string
373 init from_string(query: String) do
374 self.query = query
375 end
376
377 # init the query with parameters
378 init with_params(params: JsonObject) do
379 self.params = params
380 end
381
382 # Pass the argument `value` as the parameter `key`.
383 #
384 # SEE: `set`
385 fun []=(key: String, value: nullable Serializable) do
386 params[key] = value
387 end
388
389 # Add a `CREATE` statement to the query
390 fun ncreate(query: String): CypherQuery do
391 self.query = "{self.query}CREATE {query} "
392 return self
393 end
394
395 # Add a `START` statement to the query
396 fun nstart(query: String): CypherQuery do
397 self.query = "{self.query}START {query} "
398 return self
399 end
400
401 # Add a `MATCH` statement to the query
402 fun nmatch(query: String): CypherQuery do
403 self.query = "{self.query}MATCH {query} "
404 return self
405 end
406
407 # Add a `WHERE` statement to the query
408 fun nwhere(query: String): CypherQuery do
409 self.query = "{self.query}WHERE {query} "
410 return self
411 end
412
413 # Add a `AND` statement to the query
414 fun nand(query: String): CypherQuery do
415 self.query = "{self.query}AND {query} "
416 return self
417 end
418
419 # Add a `RETURN` statement to the query
420 fun nreturn(query: String): CypherQuery do
421 self.query = "{self.query}RETURN {query} "
422 return self
423 end
424
425 # Pass the argument `value` as the parameter `key`.
426 #
427 # Return `self`.
428 #
429 # ```
430 # var query = (new CypherQuery).
431 # nmatch("(n)").
432 # nwhere("n.key = \{key\}").
433 # set("key", "foo")
434 #
435 # assert query.params["key"] == "foo"
436 # ```
437 #
438 # SEE: `[]=`
439 fun set(key: String, value: nullable Serializable): SELF do
440 self[key] = value
441 return self
442 end
443
444 # Translate the query to the body of a corresponding Neo4j REST request.
445 fun to_rest: JsonObject do
446 var obj = new JsonObject
447 obj["query"] = query
448 if not params.is_empty then
449 obj["params"] = params
450 end
451 return obj
452 end
453
454 redef fun to_s do return to_rest.to_s
455 end
456
457 # The fundamental units that form a graph are nodes and relationships.
458 #
459 # Entities can have two states:
460 #
461 # * linked: the NeoEntity references an existing node or edge in Neo4j
462 # * unlinked: the NeoEntity is not yet created in Neo4j
463 #
464 # If the entity is initialized unlinked from neo4j:
465 #
466 # # Create a disconnected node
467 # var andres = new NeoNode
468 # andres["name"] = "Andres"
469 # # At this point, the node is not linked
470 # assert not andres.is_linked
471 #
472 # Then we can link the entity to the base:
473 #
474 # # Init client
475 # var client = new Neo4jClient("http://neo4j:7474")
476 # client.save_node(andres)
477 # # The node is now linked
478 # assert andres.is_linked
479 #
480 # Entities can also be loaded from Neo4j:
481 #
482 # # Get a node from Neo4j
483 # var url = andres.url.to_s
484 # var node = client.load_node(url)
485 # assert node.is_linked
486 #
487 # When working in connected mode, all reading operations are executed lazily on the base:
488 #
489 # # Get the node `name` property
490 # assert node["name"] == "Andres" # loaded lazily from base
491 abstract class NeoEntity
492 # Neo4j client connector
493 private var neo: Neo4jClient is noinit
494
495 # Entity unique URL in Neo4j REST API
496 var url: nullable String = null
497
498 # Temp id used in batch mode to update the entity
499 private var batch_id: nullable Int = null
500
501 # Load the entity from base
502 private init from_neo(neo: Neo4jClient, url: String) is nosuper do
503 self.neo = neo
504 self.url = url
505 end
506
507 # Init entity from JSON representation
508 private init from_json(neo: Neo4jClient, obj: JsonObject) is nosuper do
509 self.neo = neo
510 self.url = obj["self"].to_s
511 self.internal_properties = obj["data"].as(JsonObject)
512 end
513
514 # Create a empty (and not-connected) entity
515 init do
516 self.internal_properties = new JsonObject
517 end
518
519 # Is the entity linked to a Neo4j database?
520 fun is_linked: Bool do return url != null
521
522 # In Neo4j, both nodes and relationships can contain properties.
523 # Properties are key-value pairs where the key is a string.
524 # Property values are JSON formatted.
525 #
526 # Properties are loaded lazily
527 fun properties: JsonObject do return internal_properties or else load_properties
528
529 private var internal_properties: nullable JsonObject = null
530
531 private fun load_properties: JsonObject do
532 var obj = neo.get(url.to_s / "properties").as(JsonObject)
533 internal_properties = obj
534 return obj
535 end
536
537 # Get the entity `id` if connected to base
538 fun id: nullable Int do
539 if url == null then return null
540 return url.split("/").last.to_i
541 end
542
543 # Get the entity property at `key`
544 fun [](key: String): nullable Serializable do
545 if not properties.has_key(key) then return null
546 return properties[key]
547 end
548
549 # Set the entity property `value` at `key`
550 fun []=(key: String, value: nullable Serializable) do properties[key] = value
551
552 # Is the property `key` set?
553 fun has_key(key: String): Bool do return properties.has_key(key)
554 end
555
556 # Nodes are used to represent entities stored in base.
557 # Apart from properties and relationships (edges),
558 # nodes can also be labeled with zero or more labels.
559 #
560 # A label is a `String` that is used to group nodes into sets.
561 # All nodes labeled with the same label belongs to the same set.
562 # A node may be labeled with any number of labels, including none,
563 # making labels an optional addition to the graph.
564 #
565 # Creating new nodes:
566 #
567 # var client = new Neo4jClient("http://neo4j:7474")
568 #
569 # var andres = new NeoNode
570 # andres.labels.add "Person"
571 # andres["name"] = "Andres"
572 # andres["age"] = 22
573 # client.save_node(andres)
574 # assert andres.is_linked
575 #
576 # Get nodes from Neo4j:
577 #
578 # var url = andres.url.to_s
579 # var node = client.load_node(url)
580 # assert node["name"] == "Andres"
581 # assert node["age"].to_s.to_i == 22
582 class NeoNode
583 super NeoEntity
584
585 private var internal_labels: nullable Array[String] = null
586 private var internal_in_edges: nullable List[NeoEdge] = null
587 private var internal_out_edges: nullable List[NeoEdge] = null
588
589 init do
590 super
591 self.internal_labels = new Array[String]
592 self.internal_in_edges = new List[NeoEdge]
593 self.internal_out_edges = new List[NeoEdge]
594 end
595
596 redef fun to_s do
597 var tpl = new FlatBuffer
598 tpl.append "\{"
599 tpl.append "labels: [{labels.join(", ")}],"
600 tpl.append "data: {properties.to_json}"
601 tpl.append "\}"
602 return tpl.write_to_string
603 end
604
605 # A label is a `String` that is used to group nodes into sets.
606 # A node may be labeled with any number of labels, including none.
607 # All nodes labeled with the same label belongs to the same set.
608 #
609 # Many database queries can work with these sets instead of the whole graph,
610 # making queries easier to write and more efficient.
611 #
612 # Labels are loaded lazily
613 fun labels: Array[String] do return internal_labels or else load_labels
614
615 private fun load_labels: Array[String] do
616 var labels = new Array[String]
617 var res = neo.get(url.to_s / "labels")
618 if res isa JsonArray then
619 for val in res do labels.add val.to_s
620 end
621 internal_labels = labels
622 return labels
623 end
624
625 # Get the list of `NeoEdge` pointing to `self`
626 #
627 # Edges are loaded lazily
628 fun in_edges: List[NeoEdge] do return internal_in_edges or else load_in_edges
629
630 private fun load_in_edges: List[NeoEdge] do
631 var edges = new List[NeoEdge]
632 var res = neo.get(url.to_s / "relationships/in").as(JsonArray)
633 for obj in res do
634 edges.add(new NeoEdge.from_json(neo, obj.as(JsonObject)))
635 end
636 internal_in_edges = edges
637 return edges
638 end
639
640 # Get the list of `NeoEdge` pointing from `self`
641 #
642 # Edges are loaded lazily
643 fun out_edges: List[NeoEdge] do return internal_out_edges or else load_out_edges
644
645 private fun load_out_edges: List[NeoEdge] do
646 var edges = new List[NeoEdge]
647 var res = neo.get(url.to_s / "relationships/out")
648 for obj in res.as(JsonArray) do
649 edges.add(new NeoEdge.from_json(neo, obj.as(JsonObject)))
650 end
651 internal_out_edges = edges
652 return edges
653 end
654
655 # Get nodes pointed by `self` following a `rel_type` edge
656 fun out_nodes(rel_type: String): Array[NeoNode] do
657 var res = new Array[NeoNode]
658 for edge in out_edges do
659 if edge.rel_type == rel_type then res.add edge.to
660 end
661 return res
662 end
663
664 # Get nodes pointing to `self` following a `rel_type` edge
665 fun in_nodes(rel_type: String): Array[NeoNode] do
666 var res = new Array[NeoNode]
667 for edge in in_edges do
668 if edge.rel_type == rel_type then res.add edge.from
669 end
670 return res
671 end
672 end
673
674 # A relationship between two nodes.
675 # Relationships between nodes are a key part of a graph database.
676 # They allow for finding related data. Just like nodes, relationships can have properties.
677 #
678 # Create a relationship:
679 #
680 # var client = new Neo4jClient("http://neo4j:7474")
681 # # Create nodes
682 # var andres = new NeoNode
683 # andres["name"] = "Andres"
684 # var kate = new NeoNode
685 # kate["name"] = "Kate"
686 # # Create a relationship of type `LOVES`
687 # var loves = new NeoEdge(andres, "LOVES", kate)
688 # client.save_edge(loves)
689 # assert loves.is_linked
690 #
691 # Get an edge from DB:
692 #
693 # var url = loves.url.to_s
694 # var edge = client.load_edge(url)
695 # assert edge.from["name"].to_s == "Andres"
696 # assert edge.to["name"].to_s == "Kate"
697 class NeoEdge
698 super NeoEntity
699
700 private var internal_from: nullable NeoNode
701 private var internal_to: nullable NeoNode
702 private var internal_type: nullable String
703 private var internal_from_url: nullable String
704 private var internal_to_url: nullable String
705
706 init(from: NeoNode, rel_type: String, to: NeoNode) do
707 self.internal_from = from
708 self.internal_to = to
709 self.internal_type = rel_type
710 end
711
712 redef init from_neo(neo, url) do
713 super
714 var obj = neo.get(url).as(JsonObject)
715 self.internal_type = obj["type"].to_s
716 self.internal_from_url = obj["start"].to_s
717 self.internal_to_url = obj["end"].to_s
718 end
719
720 redef init from_json(neo, obj) do
721 super
722 self.internal_type = obj["type"].to_s
723 self.internal_from_url = obj["start"].to_s
724 self.internal_to_url = obj["end"].to_s
725 end
726
727 # Get `from` node
728 fun from: NeoNode do return internal_from or else load_from
729
730 private fun load_from: NeoNode do
731 var node = neo.load_node(internal_from_url.to_s)
732 internal_from = node
733 return node
734 end
735
736 # Get `to` node
737 fun to: NeoNode do return internal_to or else load_to
738
739 private fun load_to: NeoNode do
740 var node = neo.load_node(internal_to_url.to_s)
741 internal_to = node
742 return node
743 end
744
745 # Get edge type
746 fun rel_type: nullable String do return internal_type
747
748 # Get the JSON body of a REST request that create the relationship.
749 private fun to_rest: JsonObject do
750 var obj = new JsonObject
751 if to.is_linked then
752 obj["to"] = to.url
753 else
754 obj["to"] = "\{{to.batch_id.to_s}\}"
755 end
756 obj["type"] = rel_type
757 obj["data"] = properties
758 return obj
759 end
760 end
761
762 # Batches are used to perform multiple operations on the REST API in one cURL request.
763 # This can significantly improve performance for large insert and update operations.
764 #
765 # see: http://docs.neo4j.org/chunked/milestone/rest-api-batch-ops.html
766 #
767 # This service is transactional.
768 # If any of the operations performed fails (returns a non-2xx HTTP status code),
769 # the transaction will be rolled back and all changes will be undone.
770 #
771 # Example:
772 #
773 # var client = new Neo4jClient("http://neo4j:7474")
774 #
775 # var node1 = new NeoNode
776 # var node2 = new NeoNode
777 # var edge = new NeoEdge(node1, "TO", node2)
778 #
779 # var batch = new NeoBatch(client)
780 # batch.save_node(node1)
781 # batch.save_node(node2)
782 # batch.save_edge(edge)
783 # batch.execute
784 #
785 # assert node1.is_linked
786 # assert node2.is_linked
787 # assert edge.is_linked
788 class NeoBatch
789
790 # Neo4j client connector
791 var client: Neo4jClient
792
793 # Jobs to perform in this batch
794 #
795 # The batch service expects an array of job descriptions as input,
796 # each job description describing an action to be performed via the normal server API.
797 var jobs = new HashMap[Int, NeoJob]
798
799 # Append a new job to the batch in JSON Format
800 # see `NeoJob`
801 fun new_job(nentity: NeoEntity): NeoJob do
802 var id = jobs.length
803 var job = new NeoJob(id, nentity)
804 jobs[id] = job
805 return job
806 end
807
808 # Load a node in batch mode also load labels, data and edges
809 fun load_node(node: NeoNode) do
810 var job = new_job(node)
811 job.action = load_node_data_action
812 job.method = "GET"
813 if node.id != null then
814 job.to = "/node/{node.id.to_s}"
815 else
816 job.to = "\{{node.batch_id.to_s}\}"
817 end
818 job = new_job(node)
819 job.action = load_node_labels_action
820 job.method = "GET"
821 if node.id != null then
822 job.to = "/node/{node.id.to_s}/labels"
823 else
824 job.to = "\{{node.batch_id.to_s}\}/labels"
825 end
826 end
827
828 # Load in and out edges into node
829 fun load_node_edges(node: NeoNode) do
830 var job = new_job(node)
831 job.action = load_node_in_edges_action
832 job.method = "GET"
833 if node.id != null then
834 job.to = "/node/{node.id.to_s}/relationships/in"
835 else
836 job.to = "\{{node.batch_id.to_s}\}/relationships/in"
837 end
838 job = new_job(node)
839 job.action = load_node_out_edges_action
840 job.method = "GET"
841 if node.id != null then
842 job.to = "/node/{node.id.to_s}/relationships/out"
843 else
844 job.to = "\{{node.batch_id.to_s}\}/relationships/out"
845 end
846 end
847
848 # Create a `NeoNode` or a `NeoEdge` in batch mode.
849 fun save_entity(nentity: NeoEntity) do
850 if nentity isa NeoNode then
851 save_node(nentity)
852 else if nentity isa NeoEdge then
853 save_edge(nentity)
854 else abort
855 end
856
857 # Create a node in batch mode also create labels and edges
858 fun save_node(node: NeoNode) do
859 if node.id != null or node.batch_id != null then return
860 # create node
861 var job = new_job(node)
862 node.batch_id = job.id
863 job.action = create_node_action
864 job.method = "POST"
865 job.to = "/node"
866 job.body = node.properties
867 # add labels
868 job = new_job(node)
869 job.method = "POST"
870 job.to = "\{{node.batch_id.to_s}\}/labels"
871 job.body = new JsonArray.from(node.labels)
872 # add edges
873 #save_edges(node.out_edges)
874 end
875
876 # Create multiple nodes
877 # also create labels and edges
878 fun save_nodes(nodes: Collection[NeoNode]) do for node in nodes do save_node(node)
879
880 # Create an edge
881 # nodes `edge.from` and `edge.to` will be created if not in base
882 fun save_edge(edge: NeoEdge) do
883 if edge.id != null or edge.batch_id != null then return
884 # create nodes
885 save_node(edge.from)
886 save_node(edge.to)
887 # create edge
888 var job = new_job(edge)
889 edge.batch_id = job.id
890 job.action = create_edge_action
891 job.method = "POST"
892 if edge.from.id != null then
893 job.to = "/node/{edge.from.id.to_s}/relationships"
894 else
895 job.to = "\{{edge.from.batch_id.to_s}\}/relationships"
896 end
897 job.body = edge.to_rest
898 end
899
900 # Create multiple edges
901 fun save_edges(edges: Collection[NeoEdge]) do for edge in edges do save_edge(edge)
902
903 # Execute the batch and update local nodes
904 fun execute: List[NeoError] do
905 var request = new JsonPOST(client.batch_url)
906 # request.headers["X-Stream"] = "true"
907 var json_jobs = new JsonArray
908 for job in jobs.values do json_jobs.add job.to_rest
909 request.json_data = json_jobs
910 var response = request.execute
911 var res = client.parse_response(response)
912 return finalize_batch(res)
913 end
914
915 # Associate data from response in original nodes and edges
916 private fun finalize_batch(response: Serializable): List[NeoError] do
917 var errors = new List[NeoError]
918 if not response isa JsonArray then
919 errors.add(new NeoError("Unexpected batch response format.", "Neo4jError"))
920 return errors
921 end
922 # print " {res.length} jobs executed"
923 for res in response do
924 if not res isa JsonObject then
925 errors.add(new NeoError("Unexpected job format in batch response.", "Neo4jError"))
926 continue
927 end
928 var id = res["id"].as(Int)
929 var job = jobs[id]
930 if job.action == create_node_action then
931 var node = job.entity.as(NeoNode)
932 node.batch_id = null
933 node.url = res["location"].to_s
934 else if job.action == create_edge_action then
935 var edge = job.entity.as(NeoEdge)
936 edge.batch_id = null
937 edge.url = res["location"].to_s
938 else if job.action == load_node_data_action then
939 var node = job.entity.as(NeoNode)
940 node.internal_properties = res["body"].as(JsonObject)["data"].as(JsonObject)
941 else if job.action == load_node_labels_action then
942 var node = job.entity.as(NeoNode)
943 var labels = new Array[String]
944 for l in res["body"].as(JsonArray) do labels.add l.to_s
945 node.internal_labels = labels
946 else if job.action == load_node_in_edges_action then
947 var node = job.entity.as(NeoNode)
948 var edges = res["body"].as(JsonArray)
949 node.internal_in_edges = new List[NeoEdge]
950 for edge in edges do
951 node.internal_in_edges.add client.load_edge(edge.as(JsonObject)["self"].to_s)
952 end
953 else if job.action == load_node_out_edges_action then
954 var node = job.entity.as(NeoNode)
955 var edges = res["body"].as(JsonArray)
956 node.internal_out_edges = new List[NeoEdge]
957 for edge in edges do
958 node.internal_out_edges.add client.load_edge(edge.as(JsonObject)["self"].to_s)
959 end
960 end
961 end
962 return errors
963 end
964
965 # JobActions
966 # TODO replace with enum
967
968 private fun create_node_action: Int do return 1
969 private fun create_edge_action: Int do return 2
970 private fun load_node_data_action: Int do return 3
971 private fun load_node_labels_action: Int do return 4
972 private fun load_node_in_edges_action: Int do return 5
973 private fun load_node_out_edges_action: Int do return 6
974 end
975
976 # A job that can be executed in a `NeoBatch`
977 # This is a representation of a neo job in JSON Format
978 #
979 # Each job description should contain a `to` attribute, with a value relative to the data API root
980 # (so http://neo4j:7474/db/data/node becomes just /node), and a `method` attribute containing
981 # HTTP verb to use.
982 #
983 # Optionally you may provide a `body` attribute, and an `id` attribute to help you keep track
984 # of responses, although responses are guaranteed to be returned in the same order the job
985 # descriptions are received.
986 class NeoJob
987 # The job uniq `id`
988 var id: Int
989 # Entity targeted by the job
990 var entity: NeoEntity
991
992 init(id: Int, entity: NeoEntity) do
993 self.id = id
994 self.entity = entity
995 end
996
997 # What kind of action do the job
998 # used to attach responses to original Neo objets
999 private var action: nullable Int = null
1000
1001 # Job HTTP method: `GET`, `POST`, `PUT`, `DELETE`...
1002 var method: String
1003 # Job service target: `/node`, `/labels` etc...
1004 var to: String
1005 # Body to send with the job service request
1006 var body: nullable Serializable = null
1007
1008 # JSON formated job
1009 fun to_rest: JsonObject do
1010 var job = new JsonObject
1011 job["id"] = id
1012 job["method"] = method
1013 job["to"] = to
1014 if not body == null then
1015 job["body"] = body
1016 end
1017 return job
1018 end
1019 end