Merge: Ci: move services to specific hostnames
[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 init(base_url: String) do
79 self.base_url = base_url
80 var root = service_root
81 assert root isa JsonObject else
82 sys.stderr.write "Neo4jClientError: cannot connect to server at <{base_url}>.\n"
83 end
84 self.node_url = root["node"].to_s
85 self.batch_url = root["batch"].to_s
86 self.cypher_url = root["cypher"].to_s
87 end
88
89 fun service_root: Serializable do return get(base_url / "db/data")
90
91 # Is the connection with the Neo4j server ok?
92 fun is_ok: Bool do return service_root isa JsonObject
93
94 # Empty the graph
95 fun clear_graph do
96 cypher(new CypherQuery.from_string("MATCH (n) OPTIONAL MATCH n-[r]-() DELETE r, n"))
97 end
98
99 # Last errors
100 var errors = new Array[String]
101
102 # Nodes view stored locally
103 private var local_nodes = new HashMap[String, nullable NeoNode]
104
105 # Save the node in base
106 #
107 # var client = new Neo4jClient("http://neo4j:7474")
108 #
109 # # Create a node
110 # var andres = new NeoNode
111 # andres["name"] = "Andres"
112 # client.save_node(andres)
113 # assert andres.is_linked
114 #
115 # Once linked, nodes cannot be created twice:
116 #
117 # var oldurl = andres.url
118 # client.save_node(andres) # do nothing
119 # assert andres.url == oldurl
120 fun save_node(node: NeoNode): Bool do
121 if node.is_linked then return true
122 node.neo = self
123 var batch = new NeoBatch(self)
124 batch.save_node(node)
125 # batch.create_edges(node.out_edges)
126 var errors = batch.execute
127 if not errors.is_empty then
128 errors.add_all errors
129 return false
130 end
131 local_nodes[node.url.to_s] = node
132 return true
133 end
134
135 # Load a node from base
136 # Data, labels and edges will be loaded lazily.
137 fun load_node(url: String): NeoNode do
138 if local_nodes.has_key(url) then
139 var node = local_nodes[url]
140 if node != null then return node
141 end
142 var node = new NeoNode.from_neo(self, url)
143 local_nodes[url] = node
144 return node
145 end
146
147 # Remove the entity from base
148 fun delete_node(node: NeoNode): Bool do
149 if not node.is_linked then return false
150 var url = node.url.to_s
151 delete(url)
152 local_nodes[url] = null
153 node.url = null
154 return true
155 end
156
157 # Edges view stored locally
158 private var local_edges = new HashMap[String, nullable NeoEdge]
159
160 # Save the edge in base
161 # From and to nodes will be created.
162 #
163 # var client = new Neo4jClient("http://neo4j:7474")
164 #
165 # var andres = new NeoNode
166 # var kate = new NeoNode
167 # var edge = new NeoEdge(andres, "LOVES", kate)
168 # client.save_edge(edge)
169 # assert andres.is_linked
170 # assert kate.is_linked
171 # assert edge.is_linked
172 fun save_edge(edge: NeoEdge): Bool do
173 if edge.is_linked then return true
174 edge.neo = self
175 edge.from.out_edges.add edge
176 edge.to.in_edges.add edge
177 var batch = new NeoBatch(self)
178 batch.save_edge(edge)
179 var errors = batch.execute
180 if not errors.is_empty then
181 errors.add_all errors
182 return false
183 end
184 local_edges[edge.url.to_s] = edge
185 return true
186 end
187
188 # Load a edge from base
189 # Data will be loaded lazily.
190 fun load_edge(url: String): NeoEdge do
191 if local_edges.has_key(url) then
192 var node = local_edges[url]
193 if node != null then return node
194 end
195 var edge = new NeoEdge.from_neo(self, url)
196 local_edges[url] = edge
197 return edge
198 end
199
200 # Remove the edge from base
201 fun delete_edge(edge: NeoEdge): Bool do
202 if not edge.is_linked then return false
203 var url = edge.url.to_s
204 delete(url)
205 local_edges[url] = null
206 edge.url = null
207 return true
208 end
209
210 # Retrieve all nodes with specified `lbl`
211 #
212 # var client = new Neo4jClient("http://neo4j:7474")
213 #
214 # var andres = new NeoNode
215 # andres.labels.add_all(["Human", "Male"])
216 # client.save_node(andres)
217 # var kate = new NeoNode
218 # kate.labels.add_all(["Human", "Female"])
219 # client.save_node(kate)
220 #
221 # var nodes = client.nodes_with_label("Human")
222 # assert nodes.has(andres)
223 # assert nodes.has(kate)
224 fun nodes_with_label(lbl: String): Array[NeoNode] do
225 var res = get(base_url / "db/data/label/{lbl.to_percent_encoding}/nodes")
226 var nodes = new Array[NeoNode]
227 for json in res.as(JsonArray) do
228 var obj = json.as(JsonObject)
229 var node = load_node(obj["self"].to_s)
230 node.internal_properties = obj["data"].as(JsonObject)
231 nodes.add node
232 end
233 return nodes
234 end
235
236 # Retrieve nodes belonging to all the specified `labels`.
237 #
238 # var client = new Neo4jClient("http://neo4j:7474")
239 #
240 # var andres = new NeoNode
241 # andres.labels.add_all(["Human", "Male"])
242 # client.save_node(andres)
243 # var kate = new NeoNode
244 # kate.labels.add_all(["Human", "Female"])
245 # client.save_node(kate)
246 #
247 # var nodes = client.nodes_with_labels(["Human", "Male"])
248 # assert nodes.has(andres)
249 # assert not nodes.has(kate)
250 fun nodes_with_labels(labels: Array[String]): Array[NeoNode] do
251 assert not labels.is_empty
252
253 # Build the query.
254 var buffer = new Buffer
255 buffer.append "match (n) where \{label_0\} in labels(n)"
256 for i in [1..labels.length[ do
257 buffer.append " and \{label_{i}\} in labels(n)"
258 end
259 buffer.append " return n"
260 var query = new CypherQuery.from_string(buffer.write_to_string)
261 for i in [0..labels.length[ do
262 query.params["label_{i}"] = labels[i]
263 end
264
265 # Retrieve the answer.
266 var res = cypher(query)
267 var nodes = new Array[NeoNode]
268 for json in res.as(JsonObject)["data"].as(JsonArray) do
269 var obj = json.as(JsonArray).first.as(JsonObject)
270 var node = load_node(obj["self"].to_s)
271 node.internal_properties = obj["data"].as(JsonObject)
272 nodes.add node
273 end
274 return nodes
275 end
276
277 # Perform a `CypherQuery`
278 # see: CypherQuery
279 fun cypher(query: CypherQuery): Serializable do
280 return post("{cypher_url}", query.to_rest)
281 end
282
283 # GET JSON data from `url`
284 fun get(url: String): Serializable do
285 var request = new JsonGET(url)
286 var response = request.execute
287 return parse_response(response)
288 end
289
290 # POST `params` to `url`
291 fun post(url: String, params: Serializable): Serializable do
292 var request = new JsonPOST(url)
293 request.json_data = params
294 var response = request.execute
295 return parse_response(response)
296 end
297
298 # PUT `params` at `url`
299 fun put(url: String, params: Serializable): Serializable do
300 var request = new JsonPUT(url)
301 request.json_data = params
302 var response = request.execute
303 return parse_response(response)
304 end
305
306 # DELETE `url`
307 fun delete(url: String): Serializable do
308 var request = new JsonDELETE(url)
309 var response = request.execute
310 return parse_response(response)
311 end
312
313 # Parse the cURL `response` as a JSON string
314 private fun parse_response(response: CurlResponse): Serializable do
315 if response isa CurlResponseSuccess then
316 var str = response.body_str
317 if str.is_empty then return new JsonObject
318 var res = str.parse_json
319 if res isa JsonParseError then
320 var e = new NeoError(res.to_s, "JsonParseError")
321 e.cause = res
322 return e
323 end
324 if res == null then
325 # empty response wrap it in empty object
326 return new JsonObject
327 else if res isa JsonObject and res.has_key("exception") then
328 var error = "Neo4jError::{res["exception"] or else "null"}"
329 var msg = ""
330 if res.has_key("message") then
331 msg = res["message"].to_s
332 end
333 return new NeoError(msg, error)
334 else
335 return res
336 end
337 else if response isa CurlResponseFailed then
338 return new NeoError("{response.error_msg} ({response.error_code})", "CurlError")
339 else
340 return new NeoError("Unexpected response \"{response}\".", "CurlError")
341 end
342 end
343 end
344
345 # A Cypher query for Neo4j REST API
346 #
347 # The Neo4j REST API allows querying with Cypher.
348 # The results are returned as a list of string headers (columns), and a data part,
349 # consisting of a list of all rows, every row consisting of a list of REST representations
350 # of the field value - Node, Relationship, Path or any simple value like String.
351 #
352 # Example:
353 #
354 # var client = new Neo4jClient("http://neo4j:7474")
355 # var query = new CypherQuery
356 # query.nmatch("(n)-[r:LOVES]->(m)")
357 # query.nwhere("n.name=\"Andres\"")
358 # query.nreturn("m.name")
359 # var res = client.cypher(query).as(JsonObject)
360 # assert res["data"].as(JsonArray).first.as(JsonArray).first == "Kate"
361 #
362 # For more details, see: http://docs.neo4j.org/chunked/milestone/rest-api-cypher.html
363 class CypherQuery
364 # Query string to perform
365 private var query: String = ""
366
367 # `params` to embed in the query like in prepared statements
368 var params = new JsonObject
369
370 # init the query from a query string
371 init from_string(query: String) do
372 self.query = query
373 end
374
375 # init the query with parameters
376 init with_params(params: JsonObject) do
377 self.params = params
378 end
379
380 # Pass the argument `value` as the parameter `key`.
381 #
382 # SEE: `set`
383 fun []=(key: String, value: nullable Serializable) do
384 params[key] = value
385 end
386
387 # Add a `CREATE` statement to the query
388 fun ncreate(query: String): CypherQuery do
389 self.query = "{self.query}CREATE {query} "
390 return self
391 end
392
393 # Add a `START` statement to the query
394 fun nstart(query: String): CypherQuery do
395 self.query = "{self.query}START {query} "
396 return self
397 end
398
399 # Add a `MATCH` statement to the query
400 fun nmatch(query: String): CypherQuery do
401 self.query = "{self.query}MATCH {query} "
402 return self
403 end
404
405 # Add a `WHERE` statement to the query
406 fun nwhere(query: String): CypherQuery do
407 self.query = "{self.query}WHERE {query} "
408 return self
409 end
410
411 # Add a `AND` statement to the query
412 fun nand(query: String): CypherQuery do
413 self.query = "{self.query}AND {query} "
414 return self
415 end
416
417 # Add a `RETURN` statement to the query
418 fun nreturn(query: String): CypherQuery do
419 self.query = "{self.query}RETURN {query} "
420 return self
421 end
422
423 # Pass the argument `value` as the parameter `key`.
424 #
425 # Return `self`.
426 #
427 # ```
428 # var query = (new CypherQuery).
429 # nmatch("(n)").
430 # nwhere("n.key = \{key\}").
431 # set("key", "foo")
432 #
433 # assert query.params["key"] == "foo"
434 # ```
435 #
436 # SEE: `[]=`
437 fun set(key: String, value: nullable Serializable): SELF do
438 self[key] = value
439 return self
440 end
441
442 # Translate the query to the body of a corresponding Neo4j REST request.
443 fun to_rest: 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_rest.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://neo4j: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 is noinit
492
493 # Entity unique URL in Neo4j REST API
494 var url: nullable String = null
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) is nosuper 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) is nosuper 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 Serializable 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 Serializable) 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 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://neo4j: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: {properties.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://neo4j: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 # Get the JSON body of a REST request that create the relationship.
747 private fun to_rest: JsonObject do
748 var obj = new JsonObject
749 if to.is_linked then
750 obj["to"] = to.url
751 else
752 obj["to"] = "\{{to.batch_id.to_s}\}"
753 end
754 obj["type"] = rel_type
755 obj["data"] = properties
756 return obj
757 end
758 end
759
760 # Batches are used to perform multiple operations on the REST API in one cURL request.
761 # This can significantly improve performance for large insert and update operations.
762 #
763 # see: http://docs.neo4j.org/chunked/milestone/rest-api-batch-ops.html
764 #
765 # This service is transactional.
766 # If any of the operations performed fails (returns a non-2xx HTTP status code),
767 # the transaction will be rolled back and all changes will be undone.
768 #
769 # Example:
770 #
771 # var client = new Neo4jClient("http://neo4j:7474")
772 #
773 # var node1 = new NeoNode
774 # var node2 = new NeoNode
775 # var edge = new NeoEdge(node1, "TO", node2)
776 #
777 # var batch = new NeoBatch(client)
778 # batch.save_node(node1)
779 # batch.save_node(node2)
780 # batch.save_edge(edge)
781 # batch.execute
782 #
783 # assert node1.is_linked
784 # assert node2.is_linked
785 # assert edge.is_linked
786 class NeoBatch
787
788 # Neo4j client connector
789 var client: Neo4jClient
790
791 # Jobs to perform in this batch
792 #
793 # The batch service expects an array of job descriptions as input,
794 # each job description describing an action to be performed via the normal server API.
795 var jobs = new HashMap[Int, NeoJob]
796
797 # Append a new job to the batch in JSON Format
798 # see `NeoJob`
799 fun new_job(nentity: NeoEntity): NeoJob do
800 var id = jobs.length
801 var job = new NeoJob(id, nentity)
802 jobs[id] = job
803 return job
804 end
805
806 # Load a node in batch mode also load labels, data and edges
807 fun load_node(node: NeoNode) do
808 var job = new_job(node)
809 job.action = load_node_data_action
810 job.method = "GET"
811 if node.id != null then
812 job.to = "/node/{node.id.to_s}"
813 else
814 job.to = "\{{node.batch_id.to_s}\}"
815 end
816 job = new_job(node)
817 job.action = load_node_labels_action
818 job.method = "GET"
819 if node.id != null then
820 job.to = "/node/{node.id.to_s}/labels"
821 else
822 job.to = "\{{node.batch_id.to_s}\}/labels"
823 end
824 end
825
826 # Load in and out edges into node
827 fun load_node_edges(node: NeoNode) do
828 var job = new_job(node)
829 job.action = load_node_in_edges_action
830 job.method = "GET"
831 if node.id != null then
832 job.to = "/node/{node.id.to_s}/relationships/in"
833 else
834 job.to = "\{{node.batch_id.to_s}\}/relationships/in"
835 end
836 job = new_job(node)
837 job.action = load_node_out_edges_action
838 job.method = "GET"
839 if node.id != null then
840 job.to = "/node/{node.id.to_s}/relationships/out"
841 else
842 job.to = "\{{node.batch_id.to_s}\}/relationships/out"
843 end
844 end
845
846 # Create a `NeoNode` or a `NeoEdge` in batch mode.
847 fun save_entity(nentity: NeoEntity) do
848 if nentity isa NeoNode then
849 save_node(nentity)
850 else if nentity isa NeoEdge then
851 save_edge(nentity)
852 else abort
853 end
854
855 # Create a node in batch mode also create labels and edges
856 fun save_node(node: NeoNode) do
857 if node.id != null or node.batch_id != null then return
858 # create node
859 var job = new_job(node)
860 node.batch_id = job.id
861 job.action = create_node_action
862 job.method = "POST"
863 job.to = "/node"
864 job.body = node.properties
865 # add labels
866 job = new_job(node)
867 job.method = "POST"
868 job.to = "\{{node.batch_id.to_s}\}/labels"
869 job.body = new JsonArray.from(node.labels)
870 # add edges
871 #save_edges(node.out_edges)
872 end
873
874 # Create multiple nodes
875 # also create labels and edges
876 fun save_nodes(nodes: Collection[NeoNode]) do for node in nodes do save_node(node)
877
878 # Create an edge
879 # nodes `edge.from` and `edge.to` will be created if not in base
880 fun save_edge(edge: NeoEdge) do
881 if edge.id != null or edge.batch_id != null then return
882 # create nodes
883 save_node(edge.from)
884 save_node(edge.to)
885 # create edge
886 var job = new_job(edge)
887 edge.batch_id = job.id
888 job.action = create_edge_action
889 job.method = "POST"
890 if edge.from.id != null then
891 job.to = "/node/{edge.from.id.to_s}/relationships"
892 else
893 job.to = "\{{edge.from.batch_id.to_s}\}/relationships"
894 end
895 job.body = edge.to_rest
896 end
897
898 # Create multiple edges
899 fun save_edges(edges: Collection[NeoEdge]) do for edge in edges do save_edge(edge)
900
901 # Execute the batch and update local nodes
902 fun execute: List[NeoError] do
903 var request = new JsonPOST(client.batch_url)
904 # request.headers["X-Stream"] = "true"
905 var json_jobs = new JsonArray
906 for job in jobs.values do json_jobs.add job.to_rest
907 request.json_data = json_jobs
908 var response = request.execute
909 var res = client.parse_response(response)
910 return finalize_batch(res)
911 end
912
913 # Associate data from response in original nodes and edges
914 private fun finalize_batch(response: Serializable): List[NeoError] do
915 var errors = new List[NeoError]
916 if not response isa JsonArray then
917 errors.add(new NeoError("Unexpected batch response format.", "Neo4jError"))
918 return errors
919 end
920 # print " {res.length} jobs executed"
921 for res in response do
922 if not res isa JsonObject then
923 errors.add(new NeoError("Unexpected job format in batch response.", "Neo4jError"))
924 continue
925 end
926 var id = res["id"].as(Int)
927 var job = jobs[id]
928 if job.action == create_node_action then
929 var node = job.entity.as(NeoNode)
930 node.batch_id = null
931 node.url = res["location"].to_s
932 else if job.action == create_edge_action then
933 var edge = job.entity.as(NeoEdge)
934 edge.batch_id = null
935 edge.url = res["location"].to_s
936 else if job.action == load_node_data_action then
937 var node = job.entity.as(NeoNode)
938 node.internal_properties = res["body"].as(JsonObject)["data"].as(JsonObject)
939 else if job.action == load_node_labels_action then
940 var node = job.entity.as(NeoNode)
941 var labels = new Array[String]
942 for l in res["body"].as(JsonArray) do labels.add l.to_s
943 node.internal_labels = labels
944 else if job.action == load_node_in_edges_action then
945 var node = job.entity.as(NeoNode)
946 var edges = res["body"].as(JsonArray)
947 node.internal_in_edges = new List[NeoEdge]
948 for edge in edges do
949 node.internal_in_edges.add client.load_edge(edge.as(JsonObject)["self"].to_s)
950 end
951 else if job.action == load_node_out_edges_action then
952 var node = job.entity.as(NeoNode)
953 var edges = res["body"].as(JsonArray)
954 node.internal_out_edges = new List[NeoEdge]
955 for edge in edges do
956 node.internal_out_edges.add client.load_edge(edge.as(JsonObject)["self"].to_s)
957 end
958 end
959 end
960 return errors
961 end
962
963 # JobActions
964 # TODO replace with enum
965
966 private fun create_node_action: Int do return 1
967 private fun create_edge_action: Int do return 2
968 private fun load_node_data_action: Int do return 3
969 private fun load_node_labels_action: Int do return 4
970 private fun load_node_in_edges_action: Int do return 5
971 private fun load_node_out_edges_action: Int do return 6
972 end
973
974 # A job that can be executed in a `NeoBatch`
975 # This is a representation of a neo job in JSON Format
976 #
977 # Each job description should contain a `to` attribute, with a value relative to the data API root
978 # (so http://neo4j:7474/db/data/node becomes just /node), and a `method` attribute containing
979 # HTTP verb to use.
980 #
981 # Optionally you may provide a `body` attribute, and an `id` attribute to help you keep track
982 # of responses, although responses are guaranteed to be returned in the same order the job
983 # descriptions are received.
984 class NeoJob
985 # The job uniq `id`
986 var id: Int
987 # Entity targeted by the job
988 var entity: NeoEntity
989
990 init(id: Int, entity: NeoEntity) do
991 self.id = id
992 self.entity = entity
993 end
994
995 # What kind of action do the job
996 # used to attach responses to original Neo objets
997 private var action: nullable Int = null
998
999 # Job HTTP method: `GET`, `POST`, `PUT`, `DELETE`...
1000 var method: String
1001 # Job service target: `/node`, `/labels` etc...
1002 var to: String
1003 # Body to send with the job service request
1004 var body: nullable Serializable = null
1005
1006 # JSON formated job
1007 fun to_rest: JsonObject do
1008 var job = new JsonObject
1009 job["id"] = id
1010 job["method"] = method
1011 job["to"] = to
1012 if not body == null then
1013 job["body"] = body
1014 end
1015 return job
1016 end
1017 end