41fc26bf76a253ddb71278c4c090e4dcf3aeeb67
[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 # For ease of use and testing this module provide a wrapper to the `neo4j` command:
18 #
19 # # Start the Neo4j server
20 # var srv = new Neo4jServer
21 # assert srv.start_quiet
22 #
23 # In order to connect to Neo4j you need a connector:
24 #
25 # # Create new Neo4j client
26 # var client = new Neo4jClient("http://localhost:7474")
27 # assert client.is_ok
28 #
29 # The fundamental units that form a graph are nodes and relationships.
30 #
31 # Nodes are used to represent entities stored in base:
32 #
33 # # Create a disconnected node
34 # var andres = new NeoNode
35 # andres["name"] = "Andres"
36 # # Connect the node to Neo4j
37 # client.save_node(andres)
38 # assert andres.is_linked
39 # #
40 # # Create a second node
41 # var kate = new NeoNode
42 # kate["name"] = "Kate"
43 # client.save_node(kate)
44 # assert kate.is_linked
45 #
46 # Relationships between nodes are a key part of a graph database.
47 # They allow for finding related data. Just like nodes, relationships can have properties.
48 #
49 # # Create a relationship
50 # var loves = new NeoEdge(andres, "LOVES", kate)
51 # client.save_edge(loves)
52 # assert loves.is_linked
53 #
54 # Nodes can also be loaded fron Neo4j:
55 #
56 # # Get a node from DB and explore edges
57 # var url = andres.url.to_s
58 # var from = client.load_node(url)
59 # assert from["name"].to_s == "Andres"
60 # var to = from.out_nodes("LOVES").first # follow the first LOVES relationship
61 # assert to["name"].to_s == "Kate"
62 #
63 # For more details, see http://docs.neo4j.org/chunked/milestone/rest-api.html
64 module neo4j
65
66 import curl_json
67
68 # Handles Neo4j server start and stop command
69 #
70 # `neo4j` binary must be in `PATH` in order to work
71 class Neo4jServer
72
73 # Start the local Neo4j server instance
74 fun start: Bool do
75 sys.system("neo4j start console")
76 return true
77 end
78
79 # Like `start` but redirect the console output to `/dev/null`
80 fun start_quiet: Bool do
81 sys.system("neo4j start console > /dev/null")
82 return true
83 end
84
85 # Stop the local Neo4j server instance
86 fun stop: Bool do
87 sys.system("neo4j stop")
88 return true
89 end
90
91 # Like `stop` but redirect the console output to `/dev/null`
92 fun stop_quiet: Bool do
93 sys.system("neo4j stop > /dev/null")
94 return true
95 end
96 end
97
98 # `Neo4jClient` is needed to communicate through the REST API
99 #
100 # var client = new Neo4jClient("http://localhost:7474")
101 # assert client.is_ok
102 class Neo4jClient
103
104 # Neo4j REST services baseurl
105 var base_url: String
106 # REST service to get node data
107 private var node_url: String
108 # REST service to batch
109 private var batch_url: String
110 # REST service to send cypher requests
111 private var cypher_url: String
112
113 private var curl = new Curl
114
115 init(base_url: String) do
116 self.base_url = base_url
117 var root = service_root
118 if not root isa JsonObject then
119 print "Neo4jClientError: cannot connect to server at {base_url}"
120 abort
121 end
122 self.node_url = root["node"].to_s
123 self.batch_url = root["batch"].to_s
124 self.cypher_url = root["cypher"].to_s
125 end
126
127 fun service_root: Jsonable do return get("{base_url}/db/data")
128
129 # Is the connection with the Neo4j server ok?
130 fun is_ok: Bool do return service_root isa JsonObject
131
132 # Empty the graph
133 fun clear_graph do
134 cypher(new CypherQuery.from_string("MATCH (n) OPTIONAL MATCH n-[r]-() DELETE r, n"))
135 end
136
137 # Last errors
138 var errors = new Array[String]
139
140 # Nodes view stored locally
141 private var local_nodes = new HashMap[String, nullable NeoNode]
142
143 # Save the node in base
144 #
145 # var client = new Neo4jClient("http://localhost:7474")
146 # #
147 # # Create a node
148 # var andres = new NeoNode
149 # andres["name"] = "Andres"
150 # client.save_node(andres)
151 # assert andres.is_linked
152 #
153 # Once linked, nodes cannot be created twice:
154 #
155 # var oldurl = andres.url
156 # client.save_node(andres) # do nothing
157 # assert andres.url == oldurl
158 fun save_node(node: NeoNode): Bool do
159 if node.is_linked then return true
160 node.neo = self
161 var batch = new NeoBatch(self)
162 batch.save_node(node)
163 # batch.create_edges(node.out_edges)
164 var errors = batch.execute
165 if not errors.is_empty then
166 errors.add_all errors
167 return false
168 end
169 local_nodes[node.url.to_s] = node
170 return true
171 end
172
173 # Load a node from base
174 # Data, labels and edges will be loaded lazily.
175 fun load_node(url: String): NeoNode do
176 if local_nodes.has_key(url) then
177 var node = local_nodes[url]
178 if node != null then return node
179 end
180 var node = new NeoNode.from_neo(self, url)
181 local_nodes[url] = node
182 return node
183 end
184
185 # Remove the entity from base
186 fun delete_node(node: NeoNode): Bool do
187 if not node.is_linked then return false
188 var url = node.url.to_s
189 delete(url)
190 local_nodes[url] = null
191 node.url = null
192 return true
193 end
194
195 # Edges view stored locally
196 private var local_edges = new HashMap[String, nullable NeoEdge]
197
198 # Save the edge in base
199 # From and to nodes will be created.
200 #
201 # var client = new Neo4jClient("http://localhost:7474")
202 # #
203 # var andres = new NeoNode
204 # var kate = new NeoNode
205 # var edge = new NeoEdge(andres, "LOVES", kate)
206 # client.save_edge(edge)
207 # assert andres.is_linked
208 # assert kate.is_linked
209 # assert edge.is_linked
210 fun save_edge(edge: NeoEdge): Bool do
211 if edge.is_linked then return true
212 edge.neo = self
213 edge.from.out_edges.add edge
214 edge.to.in_edges.add edge
215 var batch = new NeoBatch(self)
216 batch.save_edge(edge)
217 var errors = batch.execute
218 if not errors.is_empty then
219 errors.add_all errors
220 return false
221 end
222 local_edges[edge.url.to_s] = edge
223 return true
224 end
225
226 # Load a edge from base
227 # Data will be loaded lazily.
228 fun load_edge(url: String): NeoEdge do
229 if local_edges.has_key(url) then
230 var node = local_edges[url]
231 if node != null then return node
232 end
233 var edge = new NeoEdge.from_neo(self, url)
234 local_edges[url] = edge
235 return edge
236 end
237
238 # Remove the edge from base
239 fun delete_edge(edge: NeoEdge): Bool do
240 if not edge.is_linked then return false
241 var url = edge.url.to_s
242 delete(url)
243 local_edges[url] = null
244 edge.url = null
245 return true
246 end
247
248 # Retrieve all nodes with specified `lbl`
249 #
250 # var client = new Neo4jClient("http://localhost:7474")
251 # #
252 # var andres = new NeoNode
253 # andres.labels.add_all(["Human", "Male"])
254 # client.save_node(andres)
255 # var kate = new NeoNode
256 # kate.labels.add_all(["Human", "Female"])
257 # client.save_node(kate)
258 # #
259 # var nodes = client.nodes_with_label("Human")
260 # assert nodes.has(andres)
261 # assert nodes.has(kate)
262 fun nodes_with_label(lbl: String): Array[NeoNode] do
263 var res = get("{base_url}/db/data/label/{lbl}/nodes")
264 var nodes = new Array[NeoNode]
265 for json in res.as(JsonArray) do
266 var obj = json.as(JsonObject)
267 var node = load_node(obj["self"].to_s)
268 node.internal_properties = obj["data"].as(JsonObject)
269 nodes.add node
270 end
271 return nodes
272 end
273
274 # Retrieve nodes belonging to all the specified `labels`.
275 #
276 # var client = new Neo4jClient("http://localhost:7474")
277 # #
278 # var andres = new NeoNode
279 # andres.labels.add_all(["Human", "Male"])
280 # client.save_node(andres)
281 # var kate = new NeoNode
282 # kate.labels.add_all(["Human", "Female"])
283 # client.save_node(kate)
284 # #
285 # var nodes = client.nodes_with_labels(["Human", "Male"])
286 # assert nodes.has(andres)
287 # assert not nodes.has(kate)
288 fun nodes_with_labels(labels: Array[String]): Array[NeoNode] do
289 assert not labels.is_empty
290 var res = cypher(new CypherQuery.from_string("MATCH (n:{labels.join(":")}) RETURN n"))
291 var nodes = new Array[NeoNode]
292 for json in res.as(JsonObject)["data"].as(JsonArray) do
293 var obj = json.as(JsonArray).first.as(JsonObject)
294 var node = load_node(obj["self"].to_s)
295 node.internal_properties = obj["data"].as(JsonObject)
296 nodes.add node
297 end
298 return nodes
299 end
300
301 # Perform a `CypherQuery`
302 # see: CypherQuery
303 fun cypher(query: CypherQuery): Jsonable do
304 return post("{cypher_url}", query.to_json)
305 end
306
307 # GET JSON data from `url`
308 fun get(url: String): Jsonable do
309 var request = new JsonGET(url, curl)
310 var response = request.execute
311 return parse_response(response)
312 end
313
314 # POST `params` to `url`
315 fun post(url: String, params: Jsonable): Jsonable do
316 var request = new JsonPOST(url, curl)
317 request.data = params
318 var response = request.execute
319 return parse_response(response)
320 end
321
322 # PUT `params` at `url`
323 fun put(url: String, params: Jsonable): Jsonable do
324 var request = new JsonPUT(url, curl)
325 request.data = params
326 var response = request.execute
327 return parse_response(response)
328 end
329
330 # DELETE `url`
331 fun delete(url: String): Jsonable do
332 var request = new JsonDELETE(url, curl)
333 var response = request.execute
334 return parse_response(response)
335 end
336
337 # Parse the cURL `response` as a JSON string
338 private fun parse_response(response: CurlResponse): Jsonable do
339 if response isa CurlResponseSuccess then
340 if response.body_str.is_empty then
341 return new JsonObject
342 else
343 var str = response.body_str
344 var res = str.to_jsonable
345 if res == null then
346 # empty response wrap it in empty object
347 return new JsonObject
348 else if res isa JsonObject and res.has_key("exception") then
349 var error = "Neo4jError::{res["exception"] or else "null"}"
350 var msg = ""
351 if res.has_key("message") then
352 msg = res["message"].to_s
353 end
354 return new JsonError(error, msg.to_json)
355 else
356 return res
357 end
358 end
359 else if response isa CurlResponseFailed then
360 return new JsonError("Curl error", "{response.error_msg} ({response.error_code})")
361 else
362 return new JsonError("Curl error", "Unexpected response '{response}'")
363 end
364 end
365 end
366
367 # A Cypher query for Neo4j REST API
368 #
369 # The Neo4j REST API allows querying with Cypher.
370 # The results are returned as a list of string headers (columns), and a data part,
371 # consisting of a list of all rows, every row consisting of a list of REST representations
372 # of the field value - Node, Relationship, Path or any simple value like String.
373 #
374 # Example:
375 #
376 # var client = new Neo4jClient("http://localhost:7474")
377 # var query = new CypherQuery
378 # query.nmatch("(n)-[r:LOVES]->(m)")
379 # query.nwhere("n.name=\"Andres\"")
380 # query.nreturn("m.name")
381 # var res = client.cypher(query).as(JsonObject)
382 # assert res["data"].as(JsonArray).first.as(JsonArray).first == "Kate"
383 #
384 # For more details, see: http://docs.neo4j.org/chunked/milestone/rest-api-cypher.html
385 class CypherQuery
386 # Query string to perform
387 private var query: String
388
389 # `params` to embed in the query like in prepared statements
390 var params = new JsonObject
391
392 init do
393 self.query = ""
394 end
395
396 # init the query from a query string
397 init from_string(query: String) do
398 self.query = query
399 end
400
401 # init the query with parameters
402 init with_params(params: JsonObject) do
403 self.params = params
404 end
405
406 # Add a `CREATE` statement to the query
407 fun ncreate(query: String): CypherQuery do
408 self.query = "{self.query}CREATE {query} "
409 return self
410 end
411
412 # Add a `START` statement to the query
413 fun nstart(query: String): CypherQuery do
414 self.query = "{self.query}START {query} "
415 return self
416 end
417
418 # Add a `MATCH` statement to the query
419 fun nmatch(query: String): CypherQuery do
420 self.query = "{self.query}MATCH {query} "
421 return self
422 end
423
424 # Add a `WHERE` statement to the query
425 fun nwhere(query: String): CypherQuery do
426 self.query = "{self.query}WHERE {query} "
427 return self
428 end
429
430 # Add a `AND` statement to the query
431 fun nand(query: String): CypherQuery do
432 self.query = "{self.query}AND {query} "
433 return self
434 end
435
436 # Add a `RETURN` statement to the query
437 fun nreturn(query: String): CypherQuery do
438 self.query = "{self.query}RETURN {query} "
439 return self
440 end
441
442 # Translate the query to JSON
443 fun to_json: JsonObject do
444 var obj = new JsonObject
445 obj["query"] = query
446 if not params.is_empty then
447 obj["params"] = params
448 end
449 return obj
450 end
451
452 redef fun to_s do return to_json.to_s
453 end
454
455 # The fundamental units that form a graph are nodes and relationships.
456 #
457 # Entities can have two states:
458 #
459 # * linked: the NeoEntity references an existing node or edge in Neo4j
460 # * unlinked: the NeoEntity is not yet created in Neo4j
461 #
462 # If the entity is initialized unlinked from neo4j:
463 #
464 # # Create a disconnected node
465 # var andres = new NeoNode
466 # andres["name"] = "Andres"
467 # # At this point, the node is not linked
468 # assert not andres.is_linked
469 #
470 # Then we can link the entity to the base:
471 #
472 # # Init client
473 # var client = new Neo4jClient("http://localhost:7474")
474 # client.save_node(andres)
475 # # The node is now linked
476 # assert andres.is_linked
477 #
478 # Entities can also be loaded from Neo4j:
479 #
480 # # Get a node from Neo4j
481 # var url = andres.url.to_s
482 # var node = client.load_node(url)
483 # assert node.is_linked
484 #
485 # When working in connected mode, all reading operations are executed lazily on the base:
486 #
487 # # Get the node `name` property
488 # assert node["name"] == "Andres" # loaded lazily from base
489 abstract class NeoEntity
490 # Neo4j client connector
491 private var neo: Neo4jClient
492
493 # Entity unique URL in Neo4j REST API
494 var url: nullable String
495
496 # Temp id used in batch mode to update the entity
497 private var batch_id: nullable Int = null
498
499 # Load the entity from base
500 private init from_neo(neo: Neo4jClient, url: String) do
501 self.neo = neo
502 self.url = url
503 end
504
505 # Init entity from JSON representation
506 private init from_json(neo: Neo4jClient, obj: JsonObject) do
507 self.neo = neo
508 self.url = obj["self"].to_s
509 self.internal_properties = obj["data"].as(JsonObject)
510 end
511
512 # Create a empty (and not-connected) entity
513 init do
514 self.internal_properties = new JsonObject
515 end
516
517 # Is the entity linked to a Neo4j database?
518 fun is_linked: Bool do return url != null
519
520 # In Neo4j, both nodes and relationships can contain properties.
521 # Properties are key-value pairs where the key is a string.
522 # Property values are JSON formatted.
523 #
524 # Properties are loaded lazily
525 fun properties: JsonObject do return internal_properties or else load_properties
526
527 private var internal_properties: nullable JsonObject = null
528
529 private fun load_properties: JsonObject do
530 var obj = neo.get("{url.to_s}/properties").as(JsonObject)
531 internal_properties = obj
532 return obj
533 end
534
535 # Get the entity `id` if connected to base
536 fun id: nullable Int do
537 if url == null then return null
538 return url.split("/").last.to_i
539 end
540
541 # Get the entity property at `key`
542 fun [](key: String): nullable Jsonable do
543 if not properties.has_key(key) then return null
544 return properties[key]
545 end
546
547 # Set the entity property `value` at `key`
548 fun []=(key: String, value: nullable Jsonable) do properties[key] = value
549
550 # Is the property `key` set?
551 fun has_key(key: String): Bool do return properties.has_key(key)
552
553 # Translate `self` to JSON
554 fun to_json: JsonObject do return properties
555 end
556
557 # Nodes are used to represent entities stored in base.
558 # Apart from properties and relationships (edges),
559 # nodes can also be labeled with zero or more labels.
560 #
561 # A label is a `String` that is used to group nodes into sets.
562 # All nodes labeled with the same label belongs to the same set.
563 # A node may be labeled with any number of labels, including none,
564 # making labels an optional addition to the graph.
565 #
566 # Creating new nodes:
567 #
568 # var client = new Neo4jClient("http://localhost:7474")
569 # #
570 # var andres = new NeoNode
571 # andres.labels.add "Person"
572 # andres["name"] = "Andres"
573 # andres["age"] = 22
574 # client.save_node(andres)
575 # assert andres.is_linked
576 #
577 # Get nodes from Neo4j:
578 #
579 # var url = andres.url.to_s
580 # var node = client.load_node(url)
581 # assert node["name"] == "Andres"
582 # assert node["age"].to_s.to_i == 22
583 class NeoNode
584 super NeoEntity
585
586 private var internal_labels: nullable Array[String] = null
587 private var internal_in_edges: nullable List[NeoEdge] = null
588 private var internal_out_edges: nullable List[NeoEdge] = null
589
590 init do
591 super
592 self.internal_labels = new Array[String]
593 self.internal_in_edges = new List[NeoEdge]
594 self.internal_out_edges = new List[NeoEdge]
595 end
596
597 redef fun to_s do
598 var tpl = new FlatBuffer
599 tpl.append "\{"
600 tpl.append "labels: [{labels.join(", ")}],"
601 tpl.append "data: {to_json}"
602 tpl.append "\}"
603 return tpl.write_to_string
604 end
605
606 # A label is a `String` that is used to group nodes into sets.
607 # A node may be labeled with any number of labels, including none.
608 # All nodes labeled with the same label belongs to the same set.
609 #
610 # Many database queries can work with these sets instead of the whole graph,
611 # making queries easier to write and more efficient.
612 #
613 # Labels are loaded lazily
614 fun labels: Array[String] do return internal_labels or else load_labels
615
616 private fun load_labels: Array[String] do
617 var labels = new Array[String]
618 var res = neo.get("{url.to_s}/labels")
619 if res isa JsonArray then
620 for val in res do labels.add val.to_s
621 end
622 internal_labels = labels
623 return labels
624 end
625
626 # Get the list of `NeoEdge` pointing to `self`
627 #
628 # Edges are loaded lazily
629 fun in_edges: List[NeoEdge] do return internal_in_edges or else load_in_edges
630
631 private fun load_in_edges: List[NeoEdge] do
632 var edges = new List[NeoEdge]
633 var res = neo.get("{url.to_s}/relationships/in").as(JsonArray)
634 for obj in res do
635 edges.add(new NeoEdge.from_json(neo, obj.as(JsonObject)))
636 end
637 internal_in_edges = edges
638 return edges
639 end
640
641 # Get the list of `NeoEdge` pointing from `self`
642 #
643 # Edges are loaded lazily
644 fun out_edges: List[NeoEdge] do return internal_out_edges or else load_out_edges
645
646 private fun load_out_edges: List[NeoEdge] do
647 var edges = new List[NeoEdge]
648 var res = neo.get("{url.to_s}/relationships/out")
649 for obj in res.as(JsonArray) do
650 edges.add(new NeoEdge.from_json(neo, obj.as(JsonObject)))
651 end
652 internal_out_edges = edges
653 return edges
654 end
655
656 # Get nodes pointed by `self` following a `rel_type` edge
657 fun out_nodes(rel_type: String): Array[NeoNode] do
658 var res = new Array[NeoNode]
659 for edge in out_edges do
660 if edge.rel_type == rel_type then res.add edge.to
661 end
662 return res
663 end
664
665 # Get nodes pointing to `self` following a `rel_type` edge
666 fun in_nodes(rel_type: String): Array[NeoNode] do
667 var res = new Array[NeoNode]
668 for edge in in_edges do
669 if edge.rel_type == rel_type then res.add edge.from
670 end
671 return res
672 end
673 end
674
675 # A relationship between two nodes.
676 # Relationships between nodes are a key part of a graph database.
677 # They allow for finding related data. Just like nodes, relationships can have properties.
678 #
679 # Create a relationship:
680 #
681 # var client = new Neo4jClient("http://localhost:7474")
682 # # Create nodes
683 # var andres = new NeoNode
684 # andres["name"] = "Andres"
685 # var kate = new NeoNode
686 # kate["name"] = "Kate"
687 # # Create a relationship of type `LOVES`
688 # var loves = new NeoEdge(andres, "LOVES", kate)
689 # client.save_edge(loves)
690 # assert loves.is_linked
691 #
692 # Get an edge from DB:
693 #
694 # var url = loves.url.to_s
695 # var edge = client.load_edge(url)
696 # assert edge.from["name"].to_s == "Andres"
697 # assert edge.to["name"].to_s == "Kate"
698 class NeoEdge
699 super NeoEntity
700
701 private var internal_from: nullable NeoNode
702 private var internal_to: nullable NeoNode
703 private var internal_type: nullable String
704 private var internal_from_url: nullable String
705 private var internal_to_url: nullable String
706
707 init(from: NeoNode, rel_type: String, to: NeoNode) do
708 self.internal_from = from
709 self.internal_to = to
710 self.internal_type = rel_type
711 end
712
713 redef init from_neo(neo, url) do
714 super
715 var obj = neo.get(url).as(JsonObject)
716 self.internal_type = obj["type"].to_s
717 self.internal_from_url = obj["start"].to_s
718 self.internal_to_url = obj["end"].to_s
719 end
720
721 redef init from_json(neo, obj) do
722 super
723 self.internal_type = obj["type"].to_s
724 self.internal_from_url = obj["start"].to_s
725 self.internal_to_url = obj["end"].to_s
726 end
727
728 # Get `from` node
729 fun from: NeoNode do return internal_from or else load_from
730
731 private fun load_from: NeoNode do
732 var node = neo.load_node(internal_from_url.to_s)
733 internal_from = node
734 return node
735 end
736
737 # Get `to` node
738 fun to: NeoNode do return internal_to or else load_to
739
740 private fun load_to: NeoNode do
741 var node = neo.load_node(internal_to_url.to_s)
742 internal_to = node
743 return node
744 end
745
746 # Get edge type
747 fun rel_type: nullable String do return internal_type
748
749 redef fun to_json 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://localhost: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_json
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[JsonError] do
905 var request = new JsonPOST(client.batch_url, client.curl)
906 # request.headers["X-Stream"] = "true"
907 var json_jobs = new JsonArray
908 for job in jobs.values do json_jobs.add job.to_json
909 request.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: Jsonable): List[JsonError] do
917 var errors = new List[JsonError]
918 if not response isa JsonArray then
919 errors.add(new JsonError("Neo4jError", "Unexpected batch response format"))
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 JsonError("Neo4jError", "Unexpected job format in batch response"))
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://localhost: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 Jsonable = null
1007
1008 # JSON formated job
1009 fun to_json: 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
1020