X-Git-Url: http://nitlanguage.org diff --git a/lib/neo4j/neo4j.nit b/lib/neo4j/neo4j.nit index 7d36d56..4694f8d 100644 --- a/lib/neo4j/neo4j.nit +++ b/lib/neo4j/neo4j.nit @@ -64,6 +64,7 @@ module neo4j import curl_json +import error # Handles Neo4j server start and stop command # @@ -115,16 +116,15 @@ class Neo4jClient init(base_url: String) do self.base_url = base_url var root = service_root - if not root isa JsonObject then - print "Neo4jClientError: cannot connect to server at {base_url}" - abort + assert root isa JsonObject else + sys.stderr.write "Neo4jClientError: cannot connect to server at <{base_url}>.\n" end self.node_url = root["node"].to_s self.batch_url = root["batch"].to_s self.cypher_url = root["cypher"].to_s end - fun service_root: Jsonable do return get("{base_url}/db/data") + fun service_root: Jsonable do return get(base_url / "db/data") # Is the connection with the Neo4j server ok? fun is_ok: Bool do return service_root isa JsonObject @@ -246,23 +246,76 @@ class Neo4jClient end # Retrieve all nodes with specified `lbl` + # + # var client = new Neo4jClient("http://localhost:7474") + # # + # var andres = new NeoNode + # andres.labels.add_all(["Human", "Male"]) + # client.save_node(andres) + # var kate = new NeoNode + # kate.labels.add_all(["Human", "Female"]) + # client.save_node(kate) + # # + # var nodes = client.nodes_with_label("Human") + # assert nodes.has(andres) + # assert nodes.has(kate) fun nodes_with_label(lbl: String): Array[NeoNode] do - var res = get("{base_url}/db/data/label/{lbl}/nodes") + var res = get(base_url / "db/data/label/{lbl.to_percent_encoding}/nodes") var nodes = new Array[NeoNode] - var batch = new NeoBatch(self) - for obj in res.as(JsonArray) do - var node = new NeoNode.from_json(self, obj.as(JsonObject)) - batch.load_node(node) + for json in res.as(JsonArray) do + var obj = json.as(JsonObject) + var node = load_node(obj["self"].to_s) + node.internal_properties = obj["data"].as(JsonObject) + nodes.add node + end + return nodes + end + + # Retrieve nodes belonging to all the specified `labels`. + # + # var client = new Neo4jClient("http://localhost:7474") + # # + # var andres = new NeoNode + # andres.labels.add_all(["Human", "Male"]) + # client.save_node(andres) + # var kate = new NeoNode + # kate.labels.add_all(["Human", "Female"]) + # client.save_node(kate) + # # + # var nodes = client.nodes_with_labels(["Human", "Male"]) + # assert nodes.has(andres) + # assert not nodes.has(kate) + fun nodes_with_labels(labels: Array[String]): Array[NeoNode] do + assert not labels.is_empty + + # Build the query. + var buffer = new RopeBuffer + buffer.append "match n where \{label_0\} in labels(n)" + for i in [1..labels.length[ do + buffer.append " and \{label_{i}\} in labels(n)" + end + buffer.append " return n" + var query = new CypherQuery.from_string(buffer.write_to_string) + for i in [0..labels.length[ do + query.params["label_{i}"] = labels[i] + end + + # Retrieve the answer. + var res = cypher(query) + var nodes = new Array[NeoNode] + for json in res.as(JsonObject)["data"].as(JsonArray) do + var obj = json.as(JsonArray).first.as(JsonObject) + var node = load_node(obj["self"].to_s) + node.internal_properties = obj["data"].as(JsonObject) nodes.add node end - batch.execute return nodes end # Perform a `CypherQuery` # see: CypherQuery fun cypher(query: CypherQuery): Jsonable do - return post("{cypher_url}", query.to_json) + return post("{cypher_url}", query.to_rest) end # GET JSON data from `url` @@ -298,29 +351,31 @@ class Neo4jClient # Parse the cURL `response` as a JSON string private fun parse_response(response: CurlResponse): Jsonable do if response isa CurlResponseSuccess then - if response.body_str.is_empty then + var str = response.body_str + if str.is_empty then return new JsonObject + var res = str.parse_json + if res isa JsonParseError then + var e = new NeoError(res.to_s, "JsonParseError") + e.cause = res + return e + end + if res == null then + # empty response wrap it in empty object return new JsonObject - else - var str = response.body_str - var res = str.to_jsonable - if res == null then - # empty response wrap it in empty object - return new JsonObject - else if res isa JsonObject and res.has_key("exception") then - var error = "Neo4jError::{res["exception"] or else "null"}" - var msg = "" - if res.has_key("message") then - msg = res["message"].to_s - end - return new JsonError(error, msg.to_json) - else - return res + else if res isa JsonObject and res.has_key("exception") then + var error = "Neo4jError::{res["exception"] or else "null"}" + var msg = "" + if res.has_key("message") then + msg = res["message"].to_s end + return new NeoError(msg, error) + else + return res end else if response isa CurlResponseFailed then - return new JsonError("Curl error", "{response.error_msg} ({response.error_code})") + return new NeoError("{response.error_msg} ({response.error_code})", "CurlError") else - return new JsonError("Curl error", "Unexpected response '{response}'") + return new NeoError("Unexpected response \"{response}\".", "CurlError") end end end @@ -345,15 +400,11 @@ end # For more details, see: http://docs.neo4j.org/chunked/milestone/rest-api-cypher.html class CypherQuery # Query string to perform - private var query: String + private var query: String = "" # `params` to embed in the query like in prepared statements var params = new JsonObject - init do - self.query = "" - end - # init the query from a query string init from_string(query: String) do self.query = query @@ -400,8 +451,8 @@ class CypherQuery return self end - # Translate the query to JSON - fun to_json: JsonObject do + # Translate the query to the body of a corresponding Neo4j REST request. + fun to_rest: JsonObject do var obj = new JsonObject obj["query"] = query if not params.is_empty then @@ -410,7 +461,7 @@ class CypherQuery return obj end - redef fun to_s do return to_json.to_s + redef fun to_s do return to_rest.to_s end # The fundamental units that form a graph are nodes and relationships. @@ -449,10 +500,10 @@ end # assert node["name"] == "Andres" # loaded lazily from base abstract class NeoEntity # Neo4j client connector - private var neo: Neo4jClient + private var neo: Neo4jClient is noinit # Entity unique URL in Neo4j REST API - var url: nullable String + var url: nullable String = null # Temp id used in batch mode to update the entity private var batch_id: nullable Int = null @@ -488,7 +539,7 @@ abstract class NeoEntity private var internal_properties: nullable JsonObject = null private fun load_properties: JsonObject do - var obj = neo.get("{url.to_s}/properties").as(JsonObject) + var obj = neo.get(url.to_s / "properties").as(JsonObject) internal_properties = obj return obj end @@ -510,9 +561,6 @@ abstract class NeoEntity # Is the property `key` set? fun has_key(key: String): Bool do return properties.has_key(key) - - # Translate `self` to JSON - fun to_json: JsonObject do return properties end # Nodes are used to represent entities stored in base. @@ -559,7 +607,7 @@ class NeoNode var tpl = new FlatBuffer tpl.append "\{" tpl.append "labels: [{labels.join(", ")}]," - tpl.append "data: {to_json}" + tpl.append "data: {properties.to_json}" tpl.append "\}" return tpl.write_to_string end @@ -576,7 +624,7 @@ class NeoNode private fun load_labels: Array[String] do var labels = new Array[String] - var res = neo.get("{url.to_s}/labels") + var res = neo.get(url.to_s / "labels") if res isa JsonArray then for val in res do labels.add val.to_s end @@ -591,9 +639,9 @@ class NeoNode private fun load_in_edges: List[NeoEdge] do var edges = new List[NeoEdge] - var res = neo.get("{url.to_s}/relationships/in").as(JsonArray) + var res = neo.get(url.to_s / "relationships/in").as(JsonArray) for obj in res do - edges.add(new NeoEdge.from_json(neo.as(not null), obj.as(JsonObject))) + edges.add(new NeoEdge.from_json(neo, obj.as(JsonObject))) end internal_in_edges = edges return edges @@ -606,7 +654,7 @@ class NeoNode private fun load_out_edges: List[NeoEdge] do var edges = new List[NeoEdge] - var res = neo.get("{url.to_s}/relationships/out") + var res = neo.get(url.to_s / "relationships/out") for obj in res.as(JsonArray) do edges.add(new NeoEdge.from_json(neo, obj.as(JsonObject))) end @@ -673,7 +721,7 @@ class NeoEdge redef init from_neo(neo, url) do super - var obj = neo.get(url.as(not null)).as(JsonObject) + var obj = neo.get(url).as(JsonObject) self.internal_type = obj["type"].to_s self.internal_from_url = obj["start"].to_s self.internal_to_url = obj["end"].to_s @@ -690,7 +738,7 @@ class NeoEdge fun from: NeoNode do return internal_from or else load_from private fun load_from: NeoNode do - var node = new NeoNode.from_neo(neo, internal_from_url.to_s) + var node = neo.load_node(internal_from_url.to_s) internal_from = node return node end @@ -699,7 +747,7 @@ class NeoEdge fun to: NeoNode do return internal_to or else load_to private fun load_to: NeoNode do - var node = new NeoNode.from_neo(neo, internal_to_url.to_s) + var node = neo.load_node(internal_to_url.to_s) internal_to = node return node end @@ -707,7 +755,8 @@ class NeoEdge # Get edge type fun rel_type: nullable String do return internal_type - redef fun to_json do + # Get the JSON body of a REST request that create the relationship. + private fun to_rest: JsonObject do var obj = new JsonObject if to.is_linked then obj["to"] = to.url @@ -768,13 +817,6 @@ class NeoBatch # Load a node in batch mode also load labels, data and edges fun load_node(node: NeoNode) do - load_node_data(node) - load_node_labels(node) - load_node_out_edges(node) - end - - # Load data into node - private fun load_node_data(node: NeoNode) do var job = new_job(node) job.action = load_node_data_action job.method = "GET" @@ -783,11 +825,7 @@ class NeoBatch else job.to = "\{{node.batch_id.to_s}\}" end - end - - # Load labels into node - private fun load_node_labels(node: NeoNode) do - var job = new_job(node) + job = new_job(node) job.action = load_node_labels_action job.method = "GET" if node.id != null then @@ -797,9 +835,17 @@ class NeoBatch end end - # Load out edges into node - private fun load_node_out_edges(node: NeoNode) do + # Load in and out edges into node + fun load_node_edges(node: NeoNode) do var job = new_job(node) + job.action = load_node_in_edges_action + job.method = "GET" + if node.id != null then + job.to = "/node/{node.id.to_s}/relationships/in" + else + job.to = "\{{node.batch_id.to_s}\}/relationships/in" + end + job = new_job(node) job.action = load_node_out_edges_action job.method = "GET" if node.id != null then @@ -809,6 +855,15 @@ class NeoBatch end end + # Create a `NeoNode` or a `NeoEdge` in batch mode. + fun save_entity(nentity: NeoEntity) do + if nentity isa NeoNode then + save_node(nentity) + else if nentity isa NeoEdge then + save_edge(nentity) + else abort + end + # Create a node in batch mode also create labels and edges fun save_node(node: NeoNode) do if node.id != null or node.batch_id != null then return @@ -825,7 +880,7 @@ class NeoBatch job.to = "\{{node.batch_id.to_s}\}/labels" job.body = new JsonArray.from(node.labels) # add edges - save_edges(node.out_edges) + #save_edges(node.out_edges) end # Create multiple nodes @@ -849,18 +904,18 @@ class NeoBatch else job.to = "\{{edge.from.batch_id.to_s}\}/relationships" end - job.body = edge.to_json + job.body = edge.to_rest end # Create multiple edges fun save_edges(edges: Collection[NeoEdge]) do for edge in edges do save_edge(edge) # Execute the batch and update local nodes - fun execute: List[JsonError] do + fun execute: List[NeoError] do var request = new JsonPOST(client.batch_url, client.curl) # request.headers["X-Stream"] = "true" var json_jobs = new JsonArray - for job in jobs.values do json_jobs.add job.to_json + for job in jobs.values do json_jobs.add job.to_rest request.data = json_jobs var response = request.execute var res = client.parse_response(response) @@ -868,16 +923,16 @@ class NeoBatch end # Associate data from response in original nodes and edges - private fun finalize_batch(response: Jsonable): List[JsonError] do - var errors = new List[JsonError] + private fun finalize_batch(response: Jsonable): List[NeoError] do + var errors = new List[NeoError] if not response isa JsonArray then - errors.add(new JsonError("Neo4jError", "Unexpected batch response format")) + errors.add(new NeoError("Unexpected batch response format.", "Neo4jError")) return errors end # print " {res.length} jobs executed" for res in response do if not res isa JsonObject then - errors.add(new JsonError("Neo4jError", "Unexpected job format in batch response")) + errors.add(new NeoError("Unexpected job format in batch response.", "Neo4jError")) continue end var id = res["id"].as(Int) @@ -898,12 +953,19 @@ class NeoBatch var labels = new Array[String] for l in res["body"].as(JsonArray) do labels.add l.to_s node.internal_labels = labels + else if job.action == load_node_in_edges_action then + var node = job.entity.as(NeoNode) + var edges = res["body"].as(JsonArray) + node.internal_in_edges = new List[NeoEdge] + for edge in edges do + node.internal_in_edges.add client.load_edge(edge.as(JsonObject)["self"].to_s) + end else if job.action == load_node_out_edges_action then var node = job.entity.as(NeoNode) var edges = res["body"].as(JsonArray) node.internal_out_edges = new List[NeoEdge] for edge in edges do - node.internal_out_edges.add new NeoEdge.from_json(client, edge.as(JsonObject)) + node.internal_out_edges.add client.load_edge(edge.as(JsonObject)["self"].to_s) end end end @@ -917,7 +979,8 @@ class NeoBatch private fun create_edge_action: Int do return 2 private fun load_node_data_action: Int do return 3 private fun load_node_labels_action: Int do return 4 - private fun load_node_out_edges_action: Int do return 5 + private fun load_node_in_edges_action: Int do return 5 + private fun load_node_out_edges_action: Int do return 6 end # A job that can be executed in a `NeoBatch` @@ -953,7 +1016,7 @@ class NeoJob var body: nullable Jsonable = null # JSON formated job - fun to_json: JsonObject do + fun to_rest: JsonObject do var job = new JsonObject job["id"] = id job["method"] = method