Skip to content

Commit

Permalink
Fixes neo4j-contrib#483 - Allow accessing other databases via bolt
Browse files Browse the repository at this point in the history
  • Loading branch information
AngeloBusato authored and DanielBerton committed Sep 8, 2017
1 parent 37b1ae1 commit 87d47b7
Show file tree
Hide file tree
Showing 14 changed files with 750 additions and 4 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ dependencies {
testCompile group: 'org.neo4j', name: 'neo4j-causal-clustering', version:neo4jVersion, classifier: "tests"
testCompile group: 'org.neo4j', name: 'neo4j-kernel', version:neo4jVersion, classifier: "tests"
testCompile group: 'org.neo4j', name: 'neo4j-io', version:neo4jVersion, classifier: "tests"
testCompile group: 'org.neo4j', name: 'neo4j-bolt', version: '3.0.1'

compileOnly 'org.mongodb:mongodb-driver:3.2.2'
testCompile 'org.mongodb:mongodb-driver:3.2.2'
Expand Down
138 changes: 138 additions & 0 deletions docs/bolt.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
Bolt procedures allows to accessing other databases via bolt protocol.

[cols="3m,2"]
|===
| CALL apoc.bolt.execute(urlOrKey, statement, params, config) YIELD row | access to other databases via bolt for read and write
| CALL apoc.bolt.load(urlOrKey, statement, params, config) YIELD row | access to other databases via bolt for read
|===

**urlOrKey** param allows users to decide if send url by apoc or if put it into neo4j.conf file.

* **apoc** : write the complete url in his right position on the apoc.
[source,cypher]
----
call apoc.bolt.execute("bolt://user:password@localhost:7687","match(p:Person {name:{name}}) return p", {name:'Michael'})
----

* **neo4j.conf** : here the are two choices:
1) **complete url**: write the complete url with the param apoc.bolt.url;

.apoc

[source,cypher]
----
call apoc.bolt.execute("","match(p:Person {name:{name}}) return p", {name:'Michael'})
----

.neo4jConf

[source,txt]
----
//simple url
apoc.bolt.url=bolt://neo4j:test@localhost:7687
----


2) **by key**: set the url with a personal key apoc.bolt.yourKey.url; in this case in the apoc on the url param user has to insert the key.

.apoc

[source,cypher]
----
call apoc.bolt.execute("test","match(p:Person {name:{name}}) return p", {name:'Michael'})
----

.neo4jConf

[source,txt]
----
//with key
apoc.bolt.test.url=bolt://user:password@localhost:7687
apoc.bolt.production.url=bolt://password:test@localhost:7688
----

Config available are:

* `statistics`: possible values are true/false, the default value is false. This config print the execution statistics;
* `virtual`: possible values are true/false, the default value is false. This config return result in virtual format and not in map format, in apoc.bolt.load.
== Bolt Examples

**Return node in map format**

[source,cypher]
----
call apoc.bolt.execute("bolt://user:password@localhost:7687",
"match(p:Person {name:{name}}) return p", {name:'Michael'})
----

image::{img}/apoc.bolt.execute.nodemap.png[width=800]


**Return node in virtual Node format**

[source,cypher]
----
call apoc.bolt.load("bolt://user:password@localhost:7687",
"match(p:Person {name:{name}}) return p", {name:'Michael'}, {virtual:true})
----

image::{img}/apoc.bolt.load.virtualnode.png[width=800]


**Create node and return statistic**

[source,cypher]
----
call apoc.bolt.execute("bolt://user:password@localhost:7687",
"create(n:Node {name:{name}})", {name:'Node1'}, {statistics:true})
----

image::{img}/apoc.bolt.execute.createandstatistics.png[width=800]


**Return more scalar values**

[source,cypher]
----
call apoc.bolt.execute("bolt://user:password@localhost:7687",
"match (n:Person {name:{name}}) return n.age as age, n.name as name, n.surname as surname", {name:'Michael'})
----

image::{img}/apoc.bolt.execute.scalarmulti.png[width=800]


**Return relationship in a map format**

[source,cypher]
----
call apoc.bolt.load("bolt://neo4j:test@localhost:7687",
"MATCH (n:Person{name:{name}})-[r:KNOWS]->(p) return r as rel", {name:'Anne'})
----

image::{img}/apoc.bolt.load.relmap.png[width=800]


**Return virtual path**

[source,cypher]
----
call apoc.bolt.load("bolt://user:password@localhost:7687",
"START n=node({idNode}) MATCH path= (n)-[r:REL_TYPE*..3]->(o) return path", {idNode:200}, {virtual:true})
----

image::{img}/apoc.bolt.load.returnvirtualpath.png[width=800]


**Create a Node with params in input**

[source,cypher]
----
call apoc.bolt.execute("bolt://user:password@localhost:7687",
"CREATE (n:Car{brand:{brand},model:{model},year:{year}}) return n", {brand:'Ferrari',model:'California',year:2016})
----

image::{img}/apoc.bolt.execute.createwithparams.png[width=800]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/apoc.bolt.execute.createwithparams.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/apoc.bolt.execute.nodemap.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/apoc.bolt.load.relmap.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/apoc.bolt.load.returnvirtualpath.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/apoc.bolt.load.scalarmulti.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/apoc.bolt.load.virtualnode.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions docs/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ include::schema.adoc[leveloffset=2]

include::atomic.adoc[leveloffset=1]

== Bolt

include::bolt.adoc[leveloffset=1]

== Appendix: Complete Overview

include::overview.adoc[tags=overview,leveloffset=1]
Expand Down
160 changes: 160 additions & 0 deletions src/main/java/apoc/bolt/Bolt.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package apoc.bolt;

import apoc.Description;
import apoc.result.RowResult;
import apoc.result.VirtualNode;
import apoc.result.VirtualRelationship;
import apoc.util.UriResolver;
import apoc.util.Util;
import org.neo4j.driver.internal.InternalEntity;
import org.neo4j.driver.internal.InternalPath;
import org.neo4j.driver.v1.*;
import org.neo4j.driver.v1.summary.SummaryCounters;
import org.neo4j.driver.v1.types.Node;
import org.neo4j.driver.v1.types.Path;
import org.neo4j.driver.v1.types.Relationship;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Label;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.procedure.Context;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.Procedure;

import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;

import static apoc.util.MapUtil.map;

/**
* @author AgileLARUS
* @since 29.08.17
*/
public class Bolt {

@Context
public GraphDatabaseService db;

@Procedure()
@Description("apoc.bolt.load(url-or-key, statement, params, config) - access to other databases via bolt for read")
public Stream<RowResult> load(@Name("url") String url, @Name("statement") String statement, @Name(value = "params", defaultValue = "{}") Map<String, Object> params, @Name(value = "config", defaultValue = "{}") Map<String, Object> config) throws URISyntaxException {

if (params == null) params = Collections.emptyMap();
if (config == null) config = Collections.emptyMap();
boolean virtual = (boolean) config.getOrDefault("virtual", false);
boolean addStatistics = (boolean) config.getOrDefault("statistics", false);
UriResolver uri = new UriResolver(url, "bolt");
uri.initialize();
Driver driver = null;
Session session = null;
try {
driver = GraphDatabase.driver(uri.getUrlDriver(), AuthTokens.basic(uri.getUser(), uri.getPassword()));
session = driver.session();
if (addStatistics)
return Stream.of(new RowResult(toMap(runStatement(statement, session, params).summary().counters())));

return getRowResultStream(virtual, session, params, statement);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
session.close();
driver.close();
}
}

@Procedure()
@Description("apoc.bolt.execute(url-or-key, statement, params, config) - access to other databases via bolt for read")
public Stream<RowResult> execute(@Name("url") String url, @Name("statement") String statement, @Name(value = "params", defaultValue = "{}") Map<String, Object> params, @Name(value = "config", defaultValue = "{}") Map<String, Object> config) throws URISyntaxException {
Map<String, Object> configuration = new HashMap<>(config);
configuration.remove("virtual");
return load(url, statement, params, configuration);
}

private StatementResult runStatement(@Name("statement") String statement, Session session, Map<String, Object> finalParams) {
return session.writeTransaction((Transaction tx) -> tx.run(statement, finalParams));
}

private Stream<RowResult> getRowResultStream(boolean virtual, Session session, Map<String, Object> params, String statement) {
return runStatement(statement, session, params).list().stream()
.map(record -> record.fields())
.map(fields -> {
Map<String, Object> map = new HashMap<>();
for (int i = 0; i < fields.size(); i++) {
String key = fields.get(i).key();
Object value = fields.get(i).value().asObject();
if (value instanceof Path) map.putAll(toPath(value, virtual, key));
else if (value instanceof Node) map.putAll(toNode(value, key, virtual));
else if (value instanceof Relationship) map.putAll(toRelationship(value, map, key, virtual));
else map.put(key, value);
}
return map;
})
.map(resultMap -> new RowResult(resultMap));
}

private Map<String, Object> toNode(Object value, String key, boolean virtual) {
Value internalValue = ((InternalEntity) value).asValue();
Node node = internalValue.asNode();
if (virtual) {
VirtualNode virtualNode = new VirtualNode(node.id(), db);
node.labels().forEach(l -> virtualNode.addLabel(Label.label(l)));
node.asMap().entrySet().iterator().forEachRemaining(i -> virtualNode.setProperty(i.getKey(), i.getValue()));
return Util.map(key, virtualNode);
} else
return Util.map(key, Util.map("entityType", internalValue.type().name(), "labels", node.labels(), "id", node.id(), "properties", node.asMap()));
}

private Map<String, Object> toRelationship(Object value, Map<String, Object> map, String key, boolean virtual) {
Value internalValue = ((InternalEntity) value).asValue();
Relationship relationship = internalValue.asRelationship();
if (virtual) {
VirtualNode start = new VirtualNode(relationship.startNodeId(), db);
VirtualNode end = new VirtualNode(relationship.endNodeId(), db);
if (map.values().contains(start) && map.values().contains(end))
return map;
VirtualRelationship virtualRelationship = new VirtualRelationship(start, end, RelationshipType.withName(relationship.type()));
relationship.asMap().entrySet().iterator().forEachRemaining(i -> virtualRelationship.setProperty(i.getKey(), i.getValue()));
return Util.map(key, virtualRelationship);
} else
return Util.map(key, Util.map("entityType", internalValue.type().name(), "type", relationship.type(), "id", relationship.id(), "start", relationship.startNodeId(), "end", relationship.endNodeId(), "properties", relationship.asMap()));
}

private Map<String, Object> toPath(Object value, boolean virtual, String key) {
Map<String, Object> map = new HashMap<>();
InternalPath value1 = (InternalPath) value;
Path path = value1.asValue().asPath();
path.spliterator().forEachRemaining(segment -> {
if (virtual) {
VirtualNode startNode = new VirtualNode(segment.start().id(), db);
segment.start().labels().forEach(l -> startNode.addLabel(Label.label(l)));
segment.start().asMap().entrySet().iterator().forEachRemaining(p -> startNode.setProperty(p.getKey(), p.getValue()));
VirtualNode endNode = new VirtualNode(segment.end().id(), db);
segment.end().labels().forEach(l -> endNode.addLabel(Label.label(l)));
segment.end().asMap().entrySet().iterator().forEachRemaining(p -> endNode.setProperty(p.getKey(), p.getValue()));
map.put(key, Util.map("start", startNode, "end", endNode));
} else
map.put(key, Util.map("entityType", "NODE", "startLabels", segment.start().labels(), "startId", segment.start().id(), "type", segment.relationship().type(), "endLabels", segment.end().labels(),
"endId", segment.end().id(), "startProperties", segment.start().asMap(), "relProperties", segment.relationship().asMap(), "endProperties", segment.end().asMap()));
});
return map;
}

private Map<String, Object> toMap(SummaryCounters resultSummary) {
return map(
"nodesCreated", resultSummary.nodesCreated(),
"nodesDeleted", resultSummary.nodesDeleted(),
"labelsAdded", resultSummary.labelsAdded(),
"labelsRemoved", resultSummary.labelsRemoved(),
"relationshipsCreated", resultSummary.relationshipsCreated(),
"relationshipsDeleted", resultSummary.relationshipsDeleted(),
"propertiesSet", resultSummary.propertiesSet(),
"constraintsAdded", resultSummary.constraintsAdded(),
"constraintsRemoved", resultSummary.constraintsRemoved(),
"indexesAdded", resultSummary.indexesAdded(),
"indexesRemoved", resultSummary.indexesRemoved()
);
}

}
Loading

0 comments on commit 87d47b7

Please sign in to comment.