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