From 7e3f43c37b4aba2f7a685dd115db0795144b373b Mon Sep 17 00:00:00 2001 From: =?utf8?q?Jean-Christophe=20Beaupr=C3=A9?= Date: Sat, 20 Dec 2014 19:55:35 -0500 Subject: [PATCH] neo4j/graph: Add a JSON storage mechanism. MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jean-Christophe Beaupré --- lib/neo4j/graph/json_graph_store.nit | 317 ++++++++++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 lib/neo4j/graph/json_graph_store.nit diff --git a/lib/neo4j/graph/json_graph_store.nit b/lib/neo4j/graph/json_graph_store.nit new file mode 100644 index 0000000..20736ad --- /dev/null +++ b/lib/neo4j/graph/json_graph_store.nit @@ -0,0 +1,317 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# This file is free software, which comes along with NIT. This software is +# distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. You can modify it is you want, provided this header +# is kept unaltered, and a notification of the changes is added. +# You are allowed to redistribute it and sell it, alone or is a part of +# another product. + +# Provides JSON as a mean to store graphs. +module neo4j::graph::json_graph_store + +import graph + +# Save or load a graph using a JSON document. +# +# The graph (or the specified part of it) is stored as a JSON object with the +# following properties: +# +# * `"nodes"`: An array with all nodes. Each node is an object with the +# following properties: +# * `"labels"`: An array of all applied labels. +# * `"properties"`: An object mapping each defined property to its value. +# * `"edges"`: An array with all relationships. Each relationship is an object +# with the following properties: +# * `"type"`: The type (`String`) of the relationship. +# * `"properties"`: An object mapping each defined property to its value. +# * `"from"`: The local ID of the source node. +# * `"to"`: The local ID of the destination node. +# +# ~~~nit +# import neo4j::graph::sequential_id +# +# var graph = new NeoGraph(new SequentialNodeCollection("nid")) +# var a = new NeoNode +# a.labels.add "Foo" +# a["answer"] = 42 +# a["Ultimate question of"] = new JsonArray.from(["life", +# "the Universe", "and Everything."]) +# graph.nodes.register a +# var b = graph.create_node +# b.labels.add "Foo" +# b.labels.add "Bar" +# graph.edges.add new NeoEdge(a, "BAZ", b) +# +# var ostream = new StringOStream +# var store = new JsonGraphStore(graph) +# store.ostream = ostream +# store.save +# assert ostream.to_s == """{"nodes":[""" + """ +# {"labels":["Foo"],"properties":{"answer":42,""" + """ +# "Ultimate question of":["life","the Universe","and Everything."],""" + """ +# "nid":1}},""" + """ +# {"labels":["Foo","Bar"],"properties":{"nid":2}}],""" + """ +# "edges":[{"type":"BAZ","properties":{},"from":1,"to":2}]}""" +# +# graph.nodes.clear +# graph.edges.clear +# store.istream = new StringIStream(ostream.to_s) +# store.load +# assert 1 == graph.edges.length +# for edge in graph.edges do +# assert "BAZ" == edge.rel_type +# assert a.labels == edge.from.labels +# for k, v in a.properties do assert v == edge.from.properties[k] +# assert b.labels == edge.to.labels +# for k, v in b.properties do assert v == edge.to.properties[k] +# end +# assert 2 == graph.nodes.length +# ~~~ +class JsonGraphStore + super GraphStore + + # The stream to use for `load`. + var istream: nullable IStream = null is writable + + # The stream to use for `save` and `save_part`. + var ostream: nullable OStream = null is writable + + # Use the specified `IOStream`. + init from_io(graph: NeoGraph, iostream: IOStream) do + init(graph) + istream = iostream + ostream = iostream + end + + # Use the specified string to load the graph. + init from_string(graph: NeoGraph, string: String) do + init(graph) + istream = new StringIStream(string) + end + + redef fun isolated_save do return true + + redef fun load do + var istream = self.istream + assert istream isa IStream + fire_started + graph.load_json(istream.read_all) + fire_done + end + + redef fun save_part(nodes, edges) do + var ostream = self.ostream + assert ostream isa OStream + fire_started + ostream.write(graph.to_json) + fire_done + end +end + +redef class NeoGraph + super Jsonable + + # Retrieve the graph from the specified JSON document. + # + # For the expected format, see `JsonGraphStore`. + # + # ~~~nit + # import neo4j::graph::sequential_id + # + # var graph = new NeoGraph(new SequentialNodeCollection("node_id")) + # var a = new NeoNode + # a.labels.add "Foo" + # a["answer"] = 42 + # a["Ultimate question of"] = new JsonArray.from(["life", + # "the Universe", "and Everything."]) + # graph.nodes.register a + # var b = graph.create_node + # b.labels.add "Foo" + # b.labels.add "Bar" + # graph.edges.add new NeoEdge(a, "BAZ", b) + # + # graph = new NeoGraph.from_json( + # new SequentialNodeCollection("node_id"), graph.to_json) + # assert 1 == graph.edges.length + # for edge in graph.edges do + # assert "BAZ" == edge.rel_type + # assert a.labels == edge.from.labels + # for k, v in a.properties do assert v == edge.from.properties[k] + # assert b.labels == edge.to.labels + # for k, v in b.properties do assert v == edge.to.properties[k] + # end + # assert 2 == graph.nodes.length + # ~~~ + init from_json(nodes: NeoNodeCollection, t: Text) do + from_json_object(nodes, t.parse_json.as(JsonObject)) + end + + # Retrieve the graph from the specified JSON object. + # + # For the expected format, see `JsonGraphStore`. + init from_json_object(nodes: NeoNodeCollection, o: JsonObject) do + init(nodes) + load_json_object(o) + end + + # Retrieve a part of the graph from the specified JSON document. + # + # For the expected format, see `JsonGraphStore`. + fun load_json(t: Text) do + load_json_object(t.parse_json.as(JsonObject)) + end + + # Retrieve a part of the graph from the specified JSON object. + # + # For the expected format, see `JsonGraphStore`. + fun load_json_object(o: JsonObject) do + var json_nodes = o["nodes"].as(JsonArray) + for json_node in json_nodes do + assert json_node isa JsonObject + var node = new NeoNode.from_json_object(json_node) + nodes.add node + end + + var json_edges = o["edges"].as(JsonArray) + for json_edge in json_edges do + assert json_edge isa JsonObject + var from = nodes[nodes.id_from_jsonable(json_edge["from"])] + var to = nodes[nodes.id_from_jsonable(json_edge["to"])] + var rel_type = json_edge["type"].as(String) + var json_properties = json_edge["properties"].as(JsonObject) + var edge = new NeoEdge(from, rel_type, to) + edge.properties.recover_with(json_properties) + edges.add edge + end + end + + redef fun to_json do return to_json_by_append + + # Append the JSON representation of `self` to the specified buffer. + # + # For a description of the format, see `JsonGraphStore`. + # + # SEE: `to_json` + redef fun append_json(b) do + b.append "\{\"nodes\":[" + append_entities_json(nodes, b) + b.append "],\"edges\":[" + append_entities_json(edges, b) + b.append "]\}" + end + + # Encode `self` in JSON. + # + # For a description of the format, see `JsonGraphStore`. + # + # SEE: `append_json` + private fun append_entities_json(entities: Collection[NeoEntity], + b: Buffer) do + var i = entities.iterator + if i.is_ok then + i.item.append_json_for(self, b) + i.next + for entity in i do + b.add ',' + entity.append_json_for(self, b) + end + end + end +end + +redef class NeoNodeCollection + # Convert the specified JSON value into a local ID. + fun id_from_jsonable(id: nullable Jsonable): ID_TYPE do return id.as(ID_TYPE) +end + +redef class NeoEntity + + # Append the JSON representation of the entity to the specified buffer. + fun append_json_for(graph: NeoGraph, buffer: Buffer) is abstract +end + +# Make `NeoNode` `Jsonable`. +redef class NeoNode + super Jsonable + + # Retrieve the node from the specified JSON value. + # + # Note: Here, the `"id"` is optional and ignored. + # + # SEE: `JsonGraph` + # + # var node = new NeoNode.from_json(""" + # { + # "labels": ["foo", "Bar"], + # "properties": { + # "baz": 42 + # } + # } + # """) + # assert ["foo", "Bar"] == node.labels + # assert 42 == node["baz"] + init from_json(t: Text) do + from_json_object(t.parse_json.as(JsonObject)) + end + + # Retrieve the node from the specified JSON value. + # + # Note: Here, the `"id"` is optional and ignored. + # + # SEE: `JsonGraph` + init from_json_object(o: JsonObject) do + init + var labels = o["labels"].as(JsonArray) + for lab in labels do self.labels.add(lab.as(String)) + var json_properties = o["properties"].as(JsonObject) + properties.recover_with(json_properties) + end + + redef fun to_json do return to_json_by_append + + # Append the JSON representation of the node to the specified buffer. + # + # SEE: `JsonGraph` + redef fun append_json(b) do + b.append "\{\"labels\":[" + var i = labels.iterator + if i.is_ok then + i.item.append_json(b) + i.next + for lab in i do + b.add ',' + lab.append_json(b) + end + end + b.append "],\"properties\":" + properties.append_json(b) + b.add '}' + end + + redef fun to_s do return to_json + + # Append the JSON representation of the node to the specified buffer. + redef fun append_json_for(graph: NeoGraph, buffer: Buffer) do + append_json(buffer) + end +end + +redef class NeoEdge + + # Append the JSON representation of the relationship to the specified buffer. + # + # Use the IDs specfied by `graph.nodes`. + redef fun append_json_for(graph: NeoGraph, buffer: Buffer) do + buffer.append "\{\"type\":" + rel_type.append_json(buffer) + buffer.append ",\"properties\":" + properties.append_json(buffer) + buffer.append ",\"from\":" + graph.nodes.id_of(from).append_json(buffer) + buffer.append ",\"to\":" + graph.nodes.id_of(to).append_json(buffer) + buffer.append "}" + end +end -- 1.7.9.5