X-Git-Url: http://nitlanguage.org diff --git a/lib/neo4j/neo4j.nit b/lib/neo4j/neo4j.nit index d19ba2a..a5f128a 100644 --- a/lib/neo4j/neo4j.nit +++ b/lib/neo4j/neo4j.nit @@ -14,91 +14,56 @@ # Neo4j connector through its JSON REST API using curl. # -# For ease of use and testing this module provide a wrapper to the `neo4j` command: -# -# # Start the Neo4j server -# var srv = new Neo4jServer -# assert srv.start_quiet -# # In order to connect to Neo4j you need a connector: # -# # Create new Neo4j client -# var client = new Neo4jClient("http://localhost:7474") -# assert client.is_ok +# # Create new Neo4j client +# var client = new Neo4jClient("http://neo4j:7474") +# assert client.is_ok # # The fundamental units that form a graph are nodes and relationships. # # Nodes are used to represent entities stored in base: # -# # Create a disconnected node -# var andres = new NeoNode -# andres["name"] = "Andres" -# # Connect the node to Neo4j -# client.save_node(andres) -# assert andres.is_linked -# # -# # Create a second node -# var kate = new NeoNode -# kate["name"] = "Kate" -# client.save_node(kate) -# assert kate.is_linked +# # Create a disconnected node +# var andres = new NeoNode +# andres["name"] = "Andres" +# # Connect the node to Neo4j +# client.save_node(andres) +# assert andres.is_linked +# +# # Create a second node +# var kate = new NeoNode +# kate["name"] = "Kate" +# client.save_node(kate) +# assert kate.is_linked # # Relationships between nodes are a key part of a graph database. # They allow for finding related data. Just like nodes, relationships can have properties. # -# # Create a relationship -# var loves = new NeoEdge(andres, "LOVES", kate) -# client.save_edge(loves) -# assert loves.is_linked +# # Create a relationship +# var loves = new NeoEdge(andres, "LOVES", kate) +# client.save_edge(loves) +# assert loves.is_linked # # Nodes can also be loaded fron Neo4j: # -# # Get a node from DB and explore edges -# var url = andres.url.to_s -# var from = client.load_node(url) -# assert from["name"].to_s == "Andres" -# var to = from.out_nodes("LOVES").first # follow the first LOVES relationship -# assert to["name"].to_s == "Kate" +# # Get a node from DB and explore edges +# var url = andres.url.to_s +# var from = client.load_node(url) +# assert from["name"].to_s == "Andres" +# var to = from.out_nodes("LOVES").first # follow the first LOVES relationship +# assert to["name"].to_s == "Kate" # # For more details, see http://docs.neo4j.org/chunked/milestone/rest-api.html module neo4j import curl_json - -# Handles Neo4j server start and stop command -# -# `neo4j` binary must be in `PATH` in order to work -class Neo4jServer - - # Start the local Neo4j server instance - fun start: Bool do - sys.system("neo4j start console") - return true - end - - # Like `start` but redirect the console output to `/dev/null` - fun start_quiet: Bool do - sys.system("neo4j start console > /dev/null") - return true - end - - # Stop the local Neo4j server instance - fun stop: Bool do - sys.system("neo4j stop") - return true - end - - # Like `stop` but redirect the console output to `/dev/null` - fun stop_quiet: Bool do - sys.system("neo4j stop > /dev/null") - return true - end -end +import error # `Neo4jClient` is needed to communicate through the REST API # -# var client = new Neo4jClient("http://localhost:7474") -# assert client.is_ok +# var client = new Neo4jClient("http://neo4j:7474") +# assert client.is_ok class Neo4jClient # Neo4j REST services baseurl @@ -115,16 +80,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: Serializable 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 @@ -142,8 +106,8 @@ class Neo4jClient # Save the node in base # - # var client = new Neo4jClient("http://localhost:7474") - # # + # var client = new Neo4jClient("http://neo4j:7474") + # # # Create a node # var andres = new NeoNode # andres["name"] = "Andres" @@ -198,8 +162,8 @@ class Neo4jClient # Save the edge in base # From and to nodes will be created. # - # var client = new Neo4jClient("http://localhost:7474") - # # + # var client = new Neo4jClient("http://neo4j:7474") + # # var andres = new NeoNode # var kate = new NeoNode # var edge = new NeoEdge(andres, "LOVES", kate) @@ -247,20 +211,20 @@ class Neo4jClient # Retrieve all nodes with specified `lbl` # - # var client = new Neo4jClient("http://localhost:7474") - # # + # var client = new Neo4jClient("http://neo4j: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] for json in res.as(JsonArray) do var obj = json.as(JsonObject) @@ -271,68 +235,111 @@ class Neo4jClient return nodes end + # Retrieve nodes belonging to all the specified `labels`. + # + # var client = new Neo4jClient("http://neo4j: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 Buffer + 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 + return nodes + end + # Perform a `CypherQuery` # see: CypherQuery - fun cypher(query: CypherQuery): Jsonable do - return post("{cypher_url}", query.to_json) + fun cypher(query: CypherQuery): Serializable do + return post("{cypher_url}", query.to_rest) end # GET JSON data from `url` - fun get(url: String): Jsonable do - var request = new JsonGET(url, curl) + fun get(url: String): Serializable do + var request = new JsonGET(url) var response = request.execute return parse_response(response) end # POST `params` to `url` - fun post(url: String, params: Jsonable): Jsonable do - var request = new JsonPOST(url, curl) - request.data = params + fun post(url: String, params: Serializable): Serializable do + var request = new JsonPOST(url) + request.json_data = params var response = request.execute return parse_response(response) end # PUT `params` at `url` - fun put(url: String, params: Jsonable): Jsonable do - var request = new JsonPUT(url, curl) - request.data = params + fun put(url: String, params: Serializable): Serializable do + var request = new JsonPUT(url) + request.json_data = params var response = request.execute return parse_response(response) end # DELETE `url` - fun delete(url: String): Jsonable do - var request = new JsonDELETE(url, curl) + fun delete(url: String): Serializable do + var request = new JsonDELETE(url) var response = request.execute return parse_response(response) end # Parse the cURL `response` as a JSON string - private fun parse_response(response: CurlResponse): Jsonable do + private fun parse_response(response: CurlResponse): Serializable 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 @@ -346,26 +353,22 @@ end # # Example: # -# var client = new Neo4jClient("http://localhost:7474") -# var query = new CypherQuery -# query.nmatch("(n)-[r:LOVES]->(m)") -# query.nwhere("n.name=\"Andres\"") -# query.nreturn("m.name") -# var res = client.cypher(query).as(JsonObject) -# assert res["data"].as(JsonArray).first.as(JsonArray).first == "Kate" +# var client = new Neo4jClient("http://neo4j:7474") +# var query = new CypherQuery +# query.nmatch("(n)-[r:LOVES]->(m)") +# query.nwhere("n.name=\"Andres\"") +# query.nreturn("m.name") +# var res = client.cypher(query).as(JsonObject) +# assert res["data"].as(JsonArray).first.as(JsonArray).first == "Kate" # # 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 @@ -376,6 +379,13 @@ class CypherQuery self.params = params end + # Pass the argument `value` as the parameter `key`. + # + # SEE: `set` + fun []=(key: String, value: nullable Serializable) do + params[key] = value + end + # Add a `CREATE` statement to the query fun ncreate(query: String): CypherQuery do self.query = "{self.query}CREATE {query} " @@ -412,8 +422,27 @@ class CypherQuery return self end - # Translate the query to JSON - fun to_json: JsonObject do + # Pass the argument `value` as the parameter `key`. + # + # Return `self`. + # + # ``` + # var query = (new CypherQuery). + # nmatch("(n)"). + # nwhere("n.key = \{key\}"). + # set("key", "foo") + # + # assert query.params["key"] == "foo" + # ``` + # + # SEE: `[]=` + fun set(key: String, value: nullable Serializable): SELF do + self[key] = value + return self + end + + # 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 @@ -422,7 +451,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. @@ -434,16 +463,16 @@ end # # If the entity is initialized unlinked from neo4j: # -# # Create a disconnected node -# var andres = new NeoNode -# andres["name"] = "Andres" -# # At this point, the node is not linked -# assert not andres.is_linked +# # Create a disconnected node +# var andres = new NeoNode +# andres["name"] = "Andres" +# # At this point, the node is not linked +# assert not andres.is_linked # # Then we can link the entity to the base: # # # Init client -# var client = new Neo4jClient("http://localhost:7474") +# var client = new Neo4jClient("http://neo4j:7474") # client.save_node(andres) # # The node is now linked # assert andres.is_linked @@ -461,22 +490,22 @@ 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 # Load the entity from base - private init from_neo(neo: Neo4jClient, url: String) do + private init from_neo(neo: Neo4jClient, url: String) is nosuper do self.neo = neo self.url = url end # Init entity from JSON representation - private init from_json(neo: Neo4jClient, obj: JsonObject) do + private init from_json(neo: Neo4jClient, obj: JsonObject) is nosuper do self.neo = neo self.url = obj["self"].to_s self.internal_properties = obj["data"].as(JsonObject) @@ -500,7 +529,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 @@ -512,19 +541,16 @@ abstract class NeoEntity end # Get the entity property at `key` - fun [](key: String): nullable Jsonable do + fun [](key: String): nullable Serializable do if not properties.has_key(key) then return null return properties[key] end # Set the entity property `value` at `key` - fun []=(key: String, value: nullable Jsonable) do properties[key] = value + fun []=(key: String, value: nullable Serializable) do properties[key] = value # 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. @@ -538,21 +564,21 @@ end # # Creating new nodes: # -# var client = new Neo4jClient("http://localhost:7474") -# # -# var andres = new NeoNode -# andres.labels.add "Person" -# andres["name"] = "Andres" -# andres["age"] = 22 -# client.save_node(andres) -# assert andres.is_linked +# var client = new Neo4jClient("http://neo4j:7474") +# +# var andres = new NeoNode +# andres.labels.add "Person" +# andres["name"] = "Andres" +# andres["age"] = 22 +# client.save_node(andres) +# assert andres.is_linked # # Get nodes from Neo4j: # -# var url = andres.url.to_s -# var node = client.load_node(url) -# assert node["name"] == "Andres" -# assert node["age"].to_s.to_i == 22 +# var url = andres.url.to_s +# var node = client.load_node(url) +# assert node["name"] == "Andres" +# assert node["age"].to_s.to_i == 22 class NeoNode super NeoEntity @@ -571,7 +597,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 @@ -588,7 +614,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 @@ -603,7 +629,7 @@ 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, obj.as(JsonObject))) end @@ -618,7 +644,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 @@ -651,23 +677,23 @@ end # # Create a relationship: # -# var client = new Neo4jClient("http://localhost:7474") -# # Create nodes -# var andres = new NeoNode -# andres["name"] = "Andres" -# var kate = new NeoNode -# kate["name"] = "Kate" -# # Create a relationship of type `LOVES` -# var loves = new NeoEdge(andres, "LOVES", kate) -# client.save_edge(loves) -# assert loves.is_linked +# var client = new Neo4jClient("http://neo4j:7474") +# # Create nodes +# var andres = new NeoNode +# andres["name"] = "Andres" +# var kate = new NeoNode +# kate["name"] = "Kate" +# # Create a relationship of type `LOVES` +# var loves = new NeoEdge(andres, "LOVES", kate) +# client.save_edge(loves) +# assert loves.is_linked # # Get an edge from DB: # -# var url = loves.url.to_s -# var edge = client.load_edge(url) -# assert edge.from["name"].to_s == "Andres" -# assert edge.to["name"].to_s == "Kate" +# var url = loves.url.to_s +# var edge = client.load_edge(url) +# assert edge.from["name"].to_s == "Andres" +# assert edge.to["name"].to_s == "Kate" class NeoEdge super NeoEntity @@ -719,7 +745,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 @@ -743,21 +770,21 @@ end # # Example: # -# var client = new Neo4jClient("http://localhost:7474") -# # -# var node1 = new NeoNode -# var node2 = new NeoNode -# var edge = new NeoEdge(node1, "TO", node2) -# # -# var batch = new NeoBatch(client) -# batch.save_node(node1) -# batch.save_node(node2) -# batch.save_edge(edge) -# batch.execute -# # -# assert node1.is_linked -# assert node2.is_linked -# assert edge.is_linked +# var client = new Neo4jClient("http://neo4j:7474") +# +# var node1 = new NeoNode +# var node2 = new NeoNode +# var edge = new NeoEdge(node1, "TO", node2) +# +# var batch = new NeoBatch(client) +# batch.save_node(node1) +# batch.save_node(node2) +# batch.save_edge(edge) +# batch.execute +# +# assert node1.is_linked +# assert node2.is_linked +# assert edge.is_linked class NeoBatch # Neo4j client connector @@ -818,6 +845,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 @@ -858,35 +894,35 @@ 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 - var request = new JsonPOST(client.batch_url, client.curl) + fun execute: List[NeoError] do + var request = new JsonPOST(client.batch_url) # request.headers["X-Stream"] = "true" var json_jobs = new JsonArray - for job in jobs.values do json_jobs.add job.to_json - request.data = json_jobs + for job in jobs.values do json_jobs.add job.to_rest + request.json_data = json_jobs var response = request.execute var res = client.parse_response(response) return finalize_batch(res) 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: Serializable): 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) @@ -941,7 +977,7 @@ end # This is a representation of a neo job in JSON Format # # Each job description should contain a `to` attribute, with a value relative to the data API root -# (so http://localhost:7474/db/data/node becomes just /node), and a `method` attribute containing +# (so http://neo4j:7474/db/data/node becomes just /node), and a `method` attribute containing # HTTP verb to use. # # Optionally you may provide a `body` attribute, and an `id` attribute to help you keep track @@ -967,10 +1003,10 @@ class NeoJob # Job service target: `/node`, `/labels` etc... var to: String # Body to send with the job service request - var body: nullable Jsonable = null + var body: nullable Serializable = null # JSON formated job - fun to_json: JsonObject do + fun to_rest: JsonObject do var job = new JsonObject job["id"] = id job["method"] = method @@ -981,4 +1017,3 @@ class NeoJob return job end end -