From e27287a4e1cd3de19eddb1f2fa3cb7a466fb1bc3 Mon Sep 17 00:00:00 2001 From: Michael Hunger Date: Fri, 14 Nov 2014 17:14:35 +0100 Subject: [PATCH] DATAGRAPH-483 / DATAGRAPH-253 REST Transaction support * Better abstraction layer for interfacing with Neo4j that yields equal performance in embedded and remote mode * documentation update * CypherTransaction for transactionally execution remote Cypher * RestCypherTransactionManager/RemoteCypherTransaction to integrate with JTA / JtaTransactionManager * SpringCypherRestGraphDatabase / RestAPICypherImpl encapsulating remote communication * Test update to the cypher implementation / fixes * Moving RestAPI from java-rest-binding into SDN, removing that dependency --- pom.xml | 2 +- .../neo4j/aspects/support/IndexTests.java | 8 +- .../test/resources/movies-test-context.xml | 4 +- spring-data-neo4j-rest/pom.xml | 3 +- .../rest/graphdb/CypherRestGraphDatabase.java | 4 +- .../neo4j/rest/graphdb/RestAPICypherImpl.java | 110 ++++++++++++++---- .../org/neo4j/rest/graphdb/RestAPIImpl.java | 18 ++- .../neo4j/rest/graphdb/RestAPIProvider.java | 9 ++ .../neo4j/rest/graphdb/RestGraphDatabase.java | 7 +- .../converter/RestEntityExtractor.java | 2 + .../rest/graphdb/query/CypherTransaction.java | 73 +++++++++--- .../transaction/RemoteCypherTransaction.java | 21 +++- .../rest/graphdb/util/DefaultConverter.java | 1 + .../rest/graphdb/util/QueryResultBuilder.java | 4 + .../neo4j/rest/SpringRestGraphDatabase.java | 5 + .../neo4j/rest/graphdb/EmptyGraphTest.java | 1 + .../org/neo4j/rest/graphdb/RestAPITest.java | 2 +- .../graphdb/RestCypherQueryEngineTest.java | 2 +- .../org/neo4j/rest/graphdb/RestTestBase.java | 2 +- .../rest/graphdb/ResultTypeConverterTest.java | 2 +- .../rest/graphdb/SimpleTransactionTest.java | 5 +- .../graphdb/query/CypherTransactionTest.java | 15 ++- .../query/RestCypherTransactionTest.java | 101 ++++++++++++++++ .../RestEntityPropertyValidationTests.java | 1 + .../integration/RestGraphRepositoryTests.java | 5 +- .../rest/integration/RestIndexTests.java | 21 ++++ .../RestNodeEntityRelationshipTests.java | 37 ++++++ .../rest/integration/RestNodeEntityTests.java | 5 +- .../rest/support/RestQueryEngineTests.java | 5 +- .../data/neo4j/rest/support/RestTestBase.java | 6 + .../neo4j/rest/support/RestTestHelper.java | 3 +- .../neo4j/rest/support/ServerPluginTests.java | 1 + .../support/SpringPluginInitializerTests.java | 5 +- .../src/test/resources/RestTests-context.xml | 2 +- .../neo4j/conversion/DefaultConverter.java | 2 +- .../neo4j/template/Neo4jTemplateApiTests.java | 14 ++- src/docbkx/reference/heroku.xml | 2 +- src/main/asciidoc/reference/neo4j-server.adoc | 71 +++++++---- 38 files changed, 480 insertions(+), 101 deletions(-) create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIProvider.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/query/RestCypherTransactionTest.java diff --git a/pom.xml b/pom.xml index e7a34016e..180442e31 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ 1.7 1.7 - 2.1.4 + 2.1.5 0.13-neo4j-2.1.4 2.0.1 diff --git a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/IndexTests.java b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/IndexTests.java index c54ef0e96..91432b771 100644 --- a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/IndexTests.java +++ b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/IndexTests.java @@ -52,10 +52,10 @@ import static org.springframework.data.neo4j.aspects.Person.persistedPerson; public class IndexTests extends EntityTestBase { - private static final String NAME = "name"; - private static final String NAME_VALUE = "aName"; - private static final String NAME_VALUE2 = "aSecondName"; - private static final String NAME_VALUE3 = "aThirdName"; + protected static final String NAME = "name"; + protected static final String NAME_VALUE = "aName"; + protected static final String NAME_VALUE2 = "aSecondName"; + protected static final String NAME_VALUE3 = "aThirdName"; @Test @Transactional diff --git a/spring-data-neo4j-examples/cineasts-rest/src/test/resources/movies-test-context.xml b/spring-data-neo4j-examples/cineasts-rest/src/test/resources/movies-test-context.xml index 78df40693..a65e8fc59 100644 --- a/spring-data-neo4j-examples/cineasts-rest/src/test/resources/movies-test-context.xml +++ b/spring-data-neo4j-examples/cineasts-rest/src/test/resources/movies-test-context.xml @@ -16,7 +16,7 @@ - + @@ -26,4 +26,4 @@ - \ No newline at end of file + diff --git a/spring-data-neo4j-rest/pom.xml b/spring-data-neo4j-rest/pom.xml index b13bd73ed..2a1996cb0 100644 --- a/spring-data-neo4j-rest/pom.xml +++ b/spring-data-neo4j-rest/pom.xml @@ -18,8 +18,7 @@ 1.0.0.GA 1.9 - 2.1.4 - 2.0.1 + 2.1.5 diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/CypherRestGraphDatabase.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/CypherRestGraphDatabase.java index 6ea295d97..fa03d38c4 100644 --- a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/CypherRestGraphDatabase.java +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/CypherRestGraphDatabase.java @@ -38,7 +38,7 @@ import java.util.Map; import java.util.Set; -public class CypherRestGraphDatabase extends AbstractRemoteDatabase { +public class CypherRestGraphDatabase extends AbstractRemoteDatabase implements RestAPIProvider { private RestAPICypherImpl restAPI; public CypherRestGraphDatabase(RestAPI restAPI){ @@ -121,7 +121,7 @@ public class CypherRestGraphDatabase extends AbstractRemoteDatabase { public void shutdown() { try { getTxManager().rollback(); - } catch (SystemException e) { + } catch (SystemException|IllegalStateException e) { // ignore } restAPI.close(); diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPICypherImpl.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPICypherImpl.java index 5f7632dce..a5e524abf 100644 --- a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPICypherImpl.java +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPICypherImpl.java @@ -19,9 +19,9 @@ */ package org.neo4j.rest.graphdb; +import org.apache.lucene.search.Query; import org.neo4j.graphdb.*; import org.neo4j.graphdb.index.IndexHits; -import org.neo4j.helpers.Pair; import org.neo4j.helpers.collection.IterableWrapper; import org.neo4j.helpers.collection.IteratorUtil; import org.neo4j.helpers.collection.MapUtil; @@ -40,7 +40,6 @@ import org.neo4j.rest.graphdb.util.QueryResult; import org.neo4j.rest.graphdb.util.QueryResultBuilder; import org.neo4j.rest.graphdb.util.ResultConverter; -import javax.ws.rs.core.Response.Status; import java.util.*; import static java.util.Arrays.asList; @@ -51,8 +50,8 @@ import static org.neo4j.rest.graphdb.query.CypherTransaction.Statement; public class RestAPICypherImpl implements RestAPI { - public static final String _QUERY_RETURN_NODE = " RETURN id(n) as id, labels(n) as labels, n as data"; - public static final String _QUERY_RETURN_REL = " RETURN id(r) as id, type(r) as type, r as data, id(startNode(r)) as start, id(endNode(r)) as end"; + public static final String _QUERY_RETURN_NODE = " RETURN id(n) as id, labels(n) as labels, n as properties"; + public static final String _QUERY_RETURN_REL = " RETURN id(r) as id, type(r) as type, r as properties, id(startNode(r)) as start, id(endNode(r)) as end"; public static String MATCH_NODE_QUERY(String name) { return " MATCH (" + name + ") WHERE id(" + name + ") = {id_" + name + "} "; @@ -65,6 +64,9 @@ public class RestAPICypherImpl implements RestAPI { public static final String GET_REL_TYPES_QUERY = _MATCH_NODE_QUERY + " MATCH (n)-[r]-() RETURN distinct type(r) as relType"; + private RestIndexManager restIndex = new RestIndexManager(this); + private RestIndexManager restIndexOld; + private String createNodeQuery(Collection labels) { String labelString = toLabelString(labels); return "CREATE (n" + labelString + " {props}) " + _QUERY_RETURN_NODE; @@ -96,6 +98,7 @@ public class RestAPICypherImpl implements RestAPI { protected RestAPICypherImpl(RestAPI restAPI) { this.restAPI = restAPI; + restIndexOld = new RestIndexManager(restAPI); } @Override @@ -105,7 +108,7 @@ public class RestAPICypherImpl implements RestAPI { if (restNode != null) return restNode; } if (force == Load.FromCache) return new RestNode(RestNode.nodeUri(this, id), this); - Iterator> result = query(GET_NODE_QUERY, map("id", id)).getData().iterator(); + Iterator> result = runQuery(GET_NODE_QUERY, map("id", id)).getRows().iterator(); if (!result.hasNext()) { throw new NotFoundException("Node not found " + id); } @@ -146,7 +149,7 @@ public class RestAPICypherImpl implements RestAPI { @Override public RestRelationship getRelationshipById(long id) { try { - Iterator> result = query(GET_REL_QUERY, map("id", id)).getData().iterator(); + Iterator> result = runQuery(GET_REL_QUERY, map("id", id)).getRows().iterator(); if (!result.hasNext()) { throw new NotFoundException("Relationship not found " + id); } @@ -170,7 +173,7 @@ public class RestAPICypherImpl implements RestAPI { @Override public RestNode createNode(Map props, Collection labels) { - Iterator> result = query(createNodeQuery(labels), map("props", props(props))).getData().iterator(); + Iterator> result = runQuery(createNodeQuery(labels), map("props", props(props))).getRows().iterator(); if (result.hasNext()) { return addToCache(toNode(result.next())); } @@ -184,7 +187,7 @@ public class RestAPICypherImpl implements RestAPI { nodeProperties = props(nodeProperties); Map props = nodeProperties.containsKey(key) ? nodeProperties : MapUtil.copyAndPut(nodeProperties, key, value); Map params = map("props", props, "value", value); - Iterator> result = query(mergeQuery(labelName, key, labels), params).getData().iterator(); + Iterator> result = runQuery(mergeQuery(labelName, key, labels), params).getRows().iterator(); if (!result.hasNext()) throw new RuntimeException("Error merging node with labels: " + labelName + " key " + key + " value " + value + " labels " + labels + " and props: " + props + " no data returned"); @@ -291,11 +294,10 @@ public class RestAPICypherImpl implements RestAPI { @Override public void addLabels(RestNode node, Collection labels) { String statement = _MATCH_NODE_QUERY + " SET n" + toLabelString(labels) + _QUERY_RETURN_NODE; - runQuery(statement, map("id", node.getId())); - RequestResult response = getRestRequest().with(node.getUri()).post("labels", labels); + CypherTransaction.Result result = runQuery(statement, map("id", node.getId())); - if (response.statusOtherThan(Status.NO_CONTENT)) { - throw new IllegalStateException("error adding labels, received " + response); + if (!result.hasData()) { + throw new RuntimeException("Error adding labels " + labels + " to node " + node); } } @@ -314,6 +316,7 @@ public class RestAPICypherImpl implements RestAPI { @Override public IndexHits getIndex(Class entityType, String indexName, String key, Object value) { + if (value instanceof Query) return restAPI.getIndex(entityType, indexName, key, value); String index = key == null ? ":`" + indexName + "`({query})" : ":`" + indexName + "`(`" + key + "`={query})"; if (Node.class.isAssignableFrom(entityType)) { String statement = "start n=node" + index + _QUERY_RETURN_NODE; @@ -330,7 +333,9 @@ public class RestAPICypherImpl implements RestAPI { @Override public IndexHits queryIndex(Class entityType, String indexName, String key, Object value) { + if (value instanceof Query) return restAPI.queryIndex(entityType,indexName,key,value); String index = ":`" + indexName + "`({query})"; + if (key != null && !key.isEmpty() && !value.toString().contains(":")) value = key + ":"+value; if (Node.class.isAssignableFrom(entityType)) { String statement = "start n=node" + index + _QUERY_RETURN_NODE; CypherTransaction.Result result = runQuery(statement, map("query", value)); @@ -368,7 +373,7 @@ public class RestAPICypherImpl implements RestAPI { @Override public RestIndexManager index() { - return restAPI.index(); + return restIndex; } @@ -423,7 +428,7 @@ public class RestAPICypherImpl implements RestAPI { } public CypherResult query(String statement, Map params) { - return new CypherTxResult(runQuery(statement, params)); + return new CypherTxResult(runQuery(statement, params,true)); } private List runQueries(Collection statements) { @@ -438,11 +443,14 @@ public class RestAPICypherImpl implements RestAPI { } } - private CypherTransaction.Result runQuery(String statement, Map params) { + private CypherTransaction.Result runQuery(String statement, Map params, boolean replace) { if (!txManager.isActive()) { - return newCypherTransaction().commit(statement, params); + return newCypherTransaction().commit(statement, params, replace); } - return txManager.getCypherTransaction().send(statement, params); + return txManager.getCypherTransaction().send(statement, params, replace); + } + private CypherTransaction.Result runQuery(String statement, Map params) { + return runQuery(statement,params,false); } public CypherTransaction newCypherTransaction() { @@ -450,8 +458,53 @@ public class RestAPICypherImpl implements RestAPI { } public QueryResult> query(String statement, Map params, ResultConverter resultConverter) { - CypherTransaction.Result result = runQuery(statement, params); - return new QueryResultBuilder<>(result, resultConverter); + CypherTransaction.Result result = runQuery(statement, params,true); + Iterable it = new IterableWrapper,Map>(result) { + @Override + protected Map underlyingObjectToObject(Map value) { + return convertRestEntitiesInRow(value); + } + }; + return new QueryResultBuilder<>(it, resultConverter); // new RestEntityConverter(resultConverter)); + } + + private Map convertRestEntitiesInRow(Map value) { + Map map= value; + for (Map.Entry entry : map.entrySet()) { + Object original = entry.getValue(); + if (!(original instanceof Map)) continue; + Map mapValue = (Map) original; + if (mapValue.containsKey("id") && mapValue.containsKey("properties")) { + Object v = createRestEntity(mapValue); + if (v != null) entry.setValue(v); + } + } + return map; + } + + class RestEntityConverter implements ResultConverter { + ResultConverter delegate; + + public RestEntityConverter(ResultConverter delegate) { + this.delegate = delegate; + } + + @Override + public Object convert(Object value, Class type) { + Map map= (Map) value; + for (Map.Entry entry : map.entrySet()) { + Object v = doConvert(entry.getValue(), type); + if (v != null) entry.setValue(v); + } + return map; + } + + protected Object doConvert(Object value, Class type) { + if (PropertyContainer.class.isAssignableFrom(type) && value instanceof Map) { + return createRestEntity((Map)value); + } + return delegate.convert(value,type); + } } @Override @@ -466,7 +519,10 @@ public class RestAPICypherImpl implements RestAPI { @Override @SuppressWarnings("unchecked") public RestIndex getIndex(String indexName) { - return restAPI.getIndex(indexName); + final RestIndexManager index = this.index(); + if (index.existsForNodes(indexName)) return (RestIndex) index.forNodes(indexName); + if (index.existsForRelationships(indexName)) return (RestIndex) index.forRelationships(indexName); + throw new IllegalArgumentException("Index " + indexName + " does not yet exist"); } @Override @@ -478,7 +534,13 @@ public class RestAPICypherImpl implements RestAPI { @Override @SuppressWarnings("unchecked") public RestIndex createIndex(Class type, String indexName, Map config) { - return restAPI.createIndex(type, indexName, config); + if (Node.class.isAssignableFrom(type)) { + return (RestIndex) index().forNodes( indexName, config); + } + if (Relationship.class.isAssignableFrom(type)) { + return (RestIndex) index().forRelationships(indexName, config); + } + throw new IllegalArgumentException("Required Node or Relationship types to create index, got " + type); } @Override @@ -517,8 +579,8 @@ public class RestAPICypherImpl implements RestAPI { String statement2 = MATCH_NODE_QUERY("n") + " MATCH (m) WHERE id(m) IN {ids_m} MERGE (n)"+relPattern+"(m)" + _QUERY_RETURN_REL; Map params = map("id_n", start.getId(), "ids_m", nodeIds(endNodes)); List results = runQueries(asList( - new Statement(statement1, params, row), - new Statement(statement2, params, row))); + new Statement(statement1, params, row,false), + new Statement(statement2, params, row,false))); Iterable> mergeResults = results.get(1).getRows(); return new IterableWrapper>(mergeResults) { @Override @@ -641,7 +703,7 @@ public class RestAPICypherImpl implements RestAPI { public Iterable getAllNodes() { String statement = "MATCH (n) " + _QUERY_RETURN_NODE; - Iterable> result = query(statement, null).getData(); + Iterable> result = runQuery(statement, null).getRows(); return new IterableWrapper>(result) { @Override protected Node underlyingObjectToObject(List row) { diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIImpl.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIImpl.java index 29d7e65de..e8909949e 100644 --- a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIImpl.java +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIImpl.java @@ -576,7 +576,7 @@ public class RestAPIImpl implements RestAPI { public IndexInfo indexInfo(final String indexType) { IndexInfo indexInfo = indexInfos.get(indexType); if (indexInfo != null && !indexInfo.isExpired()) { - +// return indexInfo; } RequestResult response = restRequest.get("index/" + encode(indexType)); indexInfo = new RetrievedIndexInfo(response); @@ -759,6 +759,17 @@ public class RestAPIImpl implements RestAPI { @Override public RestEntity createRestEntity(Map data) { + if (data.containsKey("id") && data.containsKey("properties")) { + long id = asLong(data, "id"); + Map props = (Map) data.get("properties"); + if (data.containsKey("type")) { + return RestRelationship.fromCypher(id,(String)data.get("type"),props,asLong(data, "startNode"),asLong(data, "endNode"),this); + } + if (data.containsKey("labels")) { + List labels = (List) data.get("labels"); + return entityCache.addToCache(RestNode.fromCypher(id,labels,props, this)); + } + } final String uri = (String) data.get("self"); if (uri == null || uri.isEmpty()) return null; if (uri.contains("/node/")) { @@ -770,6 +781,11 @@ public class RestAPIImpl implements RestAPI { return null; } + protected long asLong(Map data, String idKey) { + Object idValue = data.get(idKey); + return idValue instanceof Number ? ((Number)idValue).longValue() : Long.parseLong(idValue.toString()); + } + public RequestResult batch(Collection> batchRequestData) { return restRequest.post("batch",batchRequestData); } diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIProvider.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIProvider.java new file mode 100644 index 000000000..4a5d8a79b --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIProvider.java @@ -0,0 +1,9 @@ +package org.neo4j.rest.graphdb; + +/** + * @author mh + * @since 11.11.14 + */ +public interface RestAPIProvider { + RestAPI getRestAPI(); +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestGraphDatabase.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestGraphDatabase.java index cb81a4db2..018e66848 100644 --- a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestGraphDatabase.java +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestGraphDatabase.java @@ -36,8 +36,11 @@ import javax.transaction.TransactionManager; import java.util.Collection; import java.util.LinkedHashSet; - -public class RestGraphDatabase extends AbstractRemoteDatabase { +/** + * @deprecated use CypherRestGraphDatabase instead + */ +@Deprecated +public class RestGraphDatabase extends AbstractRemoteDatabase implements RestAPIProvider { private RestAPI restAPI; private final RestCypherQueryEngine cypherQueryEngine; diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestEntityExtractor.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestEntityExtractor.java index c94bdaa41..88a7eb283 100644 --- a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestEntityExtractor.java +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/converter/RestEntityExtractor.java @@ -21,6 +21,7 @@ package org.neo4j.rest.graphdb.converter; import java.util.*; +import org.neo4j.graphdb.PropertyContainer; import org.neo4j.helpers.collection.IterableWrapper; import org.neo4j.rest.graphdb.RequestResult; import org.neo4j.rest.graphdb.RestAPI; @@ -42,6 +43,7 @@ public class RestEntityExtractor implements RestResultConverter { @SuppressWarnings("unchecked") public Object convertFromRepresentation(Object value) { + if (value instanceof PropertyContainer) return value; if (value instanceof Map) { if (canHandle(value)) { value = convertToEntityIfPossible(value); diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/query/CypherTransaction.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/query/CypherTransaction.java index d8b116c7f..01fb27369 100644 --- a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/query/CypherTransaction.java +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/query/CypherTransaction.java @@ -1,7 +1,6 @@ package org.neo4j.rest.graphdb.query; import com.sun.jersey.api.client.ClientResponse; -import org.neo4j.helpers.Pair; import org.neo4j.helpers.collection.IterableWrapper; import org.neo4j.helpers.collection.IteratorWrapper; import org.neo4j.rest.graphdb.*; @@ -30,6 +29,14 @@ public class CypherTransaction { result.addAll((List) graph.get("relationships")); return result; } + public List getNodes(Map data) { + Map graph = (Map) data.get(name()); + return (List)graph.get("nodes"); + } + public List getRelationships(Map data) { + Map graph = (Map) data.get(name()); + return (List)graph.get("relationships"); + } }, row, rest; public List get(Map data) { @@ -69,14 +76,34 @@ public class CypherTransaction { private static Result toResult(Map resultData, Statement statement, final ResultType type) { List columns = (List) resultData.get("columns"); List rowsData = (List) resultData.get("data"); + final boolean replace = statement.doReplace(); Iterable> rows = new IterableWrapper,Map>(rowsData) { protected List underlyingObjectToObject(Map map) { - return type.get(map); + List row = type.get(map); + List graph = ResultType.graph.get(map); + if (replace) replaceGraphElements(row, (List) graph); + return row; } }; return new Result(columns, rows, statement); } + // todo hack !! + private static void replaceGraphElements(List row, List graph) { + for (Map pc : graph) { + Object props = pc.get("properties"); + int pos = -1; + for (int i = 0; i < row.size(); i++) { + Object o = row.get(i); + if (props.equals(o)) { +// row.set(i,pc); + if (pos == -1) pos = i; else pos = -2; + } + } + if (pos >= 0) row.set(pos,pc); + } + } + public List getColumns() { return columns; } @@ -111,10 +138,12 @@ public class CypherTransaction { private final String statement; private final ResultType type; private final Map parameters; + private final boolean replace; - public Statement(String query, Map parameters, ResultType type) { + public Statement(String query, Map parameters, ResultType type, boolean replace) { this.statement = query; this.type = type; + this.replace = replace; this.parameters = parameters == null ? Collections.emptyMap() : parameters; } @@ -127,8 +156,14 @@ public class CypherTransaction { } public List getResultDataContents() { - return Collections.singletonList(type.name()); + return Arrays.asList(type.name(), ResultType.graph.name()); } + + public ResultType getType() { + return type; + } + + private boolean doReplace() { return replace; } } private final ResultType type; @@ -145,23 +180,33 @@ public class CypherTransaction { this.statements.addAll(statements); } - public void add(String statement, Map params) { - statements.add(new Statement(statement,params,type)); + public void add(String statement, Map params) { + add(statement,params,false); + } + public void add(String statement, Map params, boolean replace) { + statements.add(new Statement(statement,params,type, replace)); } + public Result send(String statement, Map params) { + return send(statement,params,false); + } - public Result send(String statement, Map params) { - add(statement,params); + public Result send(String statement, Map params, boolean replace) { + add(statement,params, replace); List results = send(transactionUrl()); if (results.size() > 0) return results.get(results.size() - 1); - throw new CypherTransactionExecutionException("Error Sending",asList(new Statement(statement,params,type)),errors("No.Results","No Results after single send")); + throw new CypherTransactionExecutionException("Error Sending",asList(new Statement(statement,params,type, replace)),errors("No.Results","No Results after single send")); } - public Result commit(String statement, Map params) { - add(statement,params); + public Result commit(String statement, Map params) { + return commit(statement,params,false); + } + + public Result commit(String statement, Map params, boolean replace) { + add(statement,params, replace); List results = commit(); if (results.size() > 0) return results.get(results.size() - 1); - else throw new CypherTransactionExecutionException("Error Sending",asList(new Statement(statement,params,type)),errors("No.Results","No Results after single commit")); + else throw new CypherTransactionExecutionException("Error Sending",asList(new Statement(statement,params,type, replace)),errors("No.Results","No Results after single commit")); } public List send() { @@ -170,7 +215,7 @@ public class CypherTransaction { public List commit() { try { - if (statements.isEmpty()) add("return 1",null); // TODO hacking workaround b/c of periodic commit check in server accesses the first of an empty statement list with an NPE + if (statements.isEmpty()) add("return 1",null, false); // TODO hacking workaround b/c of periodic commit check in server accesses the first of an empty statement list with an NPE return send(commitUrl()); } finally { commitUrl = null; @@ -217,8 +262,8 @@ public class CypherTransaction { private List handleResult(RequestResult result, ArrayList statements) { Map resultData = result.toMap(); List> errors = (List>) resultData.get("errors"); - if (errors != null && !errors.isEmpty()) throw new CypherTransactionExecutionException("Error executing cypher statements ",statements, errors); if (result.statusIs(ClientResponse.Status.CREATED)) transactionUrl = result.getLocation(); + if (errors != null && !errors.isEmpty()) throw new CypherTransactionExecutionException("Error executing cypher statements ",statements, errors); commitUrl = (String) resultData.get("commit"); return (List) resultData.get("results"); } diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/transaction/RemoteCypherTransaction.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/transaction/RemoteCypherTransaction.java index 69d18375f..e6327c07f 100644 --- a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/transaction/RemoteCypherTransaction.java +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/transaction/RemoteCypherTransaction.java @@ -35,13 +35,28 @@ public class RemoteCypherTransaction implements Transaction { CypherTransaction tx; AtomicInteger innerCounter = new AtomicInteger(1); + @Override + public String toString() { + return "RemoteCypherTransaction@"+System.identityHashCode(this)+"{" + + "status=" + status + + ", success=" + success + + ", failure=" + failure + + ", tx=" + tx + + ", innerCounter=" + innerCounter + + '}'; + } + public RemoteCypherTransaction(CypherTransaction tx) { this.tx = tx; status = Status.STATUS_ACTIVE; } public void beginInner() { - innerCounter.incrementAndGet(); + if (status == Status.STATUS_ACTIVE || status == Status.STATUS_COMMITTING || status == Status.STATUS_MARKED_ROLLBACK) { + innerCounter.incrementAndGet(); + } else { + throw new IllegalStateException("Can't begin nested tx on non-active transaction status is " + status + " tx " + this); + } } public void success() { @@ -85,10 +100,10 @@ public class RemoteCypherTransaction implements Transaction { @Override public Lock acquireWriteLock(PropertyContainer pc) { if (pc instanceof Node) { - tx().send("MATCH (n) WHERE id(n) = {id} REMOVE n.` lock property `", map("id", ((Node) pc).getId())); + tx().send("MATCH (n) WHERE id(n) = {id} REMOVE n.` lock property `", map("id", ((Node) pc).getId()), false); } if (pc instanceof Relationship) { - tx().send("START r=rel({id}) REMOVE r.` lock property `", map("id", ((Relationship) pc).getId())); + tx().send("START r=rel({id}) REMOVE r.` lock property `", map("id", ((Relationship) pc).getId()), false); } return new Lock() { public void release() { } }; // release at commit } diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/DefaultConverter.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/DefaultConverter.java index 18885b6f9..b5fb8c355 100644 --- a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/DefaultConverter.java +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/DefaultConverter.java @@ -25,6 +25,7 @@ import java.util.Map; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Path; import org.neo4j.graphdb.Relationship; +import org.neo4j.rest.graphdb.entity.RestNode; import org.neo4j.rest.graphdb.traversal.NodePath; import org.neo4j.rest.graphdb.traversal.RelationshipPath; diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/QueryResultBuilder.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/QueryResultBuilder.java index 7591aea12..e6061fdc9 100644 --- a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/QueryResultBuilder.java +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/QueryResultBuilder.java @@ -20,15 +20,19 @@ package org.neo4j.rest.graphdb.util; import java.util.Iterator; +import java.util.List; import java.util.Map; +import org.neo4j.graphdb.PropertyContainer; import org.neo4j.graphdb.index.IndexHits; import org.neo4j.helpers.collection.ClosableIterable; import org.neo4j.helpers.collection.IteratorUtil; import org.neo4j.helpers.collection.IteratorWrapper; +import org.neo4j.rest.graphdb.query.CypherTransaction; public class QueryResultBuilder implements QueryResult { + private CypherTransaction.Result cypherResult; private Iterable result; private final ResultConverter defaultConverter; private final boolean isClosableIterable; diff --git a/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/SpringRestGraphDatabase.java b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/SpringRestGraphDatabase.java index 9b080262d..e100a2445 100644 --- a/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/SpringRestGraphDatabase.java +++ b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/SpringRestGraphDatabase.java @@ -40,6 +40,11 @@ import java.util.Map; import static org.neo4j.helpers.collection.MapUtil.map; + +/** + * @deprecated use SpringCypherRestGraphDatabase instead + */ +@Deprecated public class SpringRestGraphDatabase extends org.neo4j.rest.graphdb.RestGraphDatabase implements GraphDatabase { static { System.setProperty(Config.CONFIG_BATCH_TRANSACTION,"false"); diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/EmptyGraphTest.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/EmptyGraphTest.java index 8398768c8..c690bdf71 100644 --- a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/EmptyGraphTest.java +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/EmptyGraphTest.java @@ -20,6 +20,7 @@ package org.neo4j.rest.graphdb; import org.junit.After; +import org.junit.Ignore; import org.junit.Test; import org.neo4j.graphdb.NotFoundException; import org.neo4j.graphdb.Transaction; diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestAPITest.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestAPITest.java index 1e531ebcf..905325b33 100644 --- a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestAPITest.java +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestAPITest.java @@ -50,7 +50,7 @@ public class RestAPITest extends RestTestBase { @Before public void init(){ - this.restAPI = ((RestGraphDatabase)getRestGraphDb()).getRestAPI(); + this.restAPI = ((RestAPIProvider)getRestGraphDb()).getRestAPI(); } @Test diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestCypherQueryEngineTest.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestCypherQueryEngineTest.java index 608c78766..228199814 100644 --- a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestCypherQueryEngineTest.java +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestCypherQueryEngineTest.java @@ -45,7 +45,7 @@ public class RestCypherQueryEngineTest extends RestTestBase { public void init() throws Exception { embeddedMatrixdata = new MatrixDataGraph(getGraphDatabase(),nodeId()).createNodespace(); restMatrixData = new MatrixDataGraph(getRestGraphDb(),nodeId()); - this.restAPI = ((RestGraphDatabase)getRestGraphDb()).getRestAPI(); + this.restAPI = ((RestAPIProvider)getRestGraphDb()).getRestAPI(); queryEngine = new RestCypherQueryEngine(restAPI); } diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTestBase.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTestBase.java index 8ba43b93b..03cec090a 100644 --- a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTestBase.java +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTestBase.java @@ -95,7 +95,7 @@ public class RestTestBase { } protected GraphDatabaseService createRestGraphDatabase() { - return new RestGraphDatabase(SERVER_ROOT_URI); + return new CypherRestGraphDatabase(SERVER_ROOT_URI); } @After diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/ResultTypeConverterTest.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/ResultTypeConverterTest.java index 61ad679cd..80d70de91 100644 --- a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/ResultTypeConverterTest.java +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/ResultTypeConverterTest.java @@ -56,7 +56,7 @@ public class ResultTypeConverterTest extends RestTestBase { @Before public void init(){ - restAPI = ((RestGraphDatabase)getRestGraphDb()).getRestAPI(); + restAPI = ((RestAPIProvider)getRestGraphDb()).getRestAPI(); converter = new ResultTypeConverter(restAPI); } diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/SimpleTransactionTest.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/SimpleTransactionTest.java index 471b46ea1..243a58bf5 100644 --- a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/SimpleTransactionTest.java +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/SimpleTransactionTest.java @@ -3,6 +3,7 @@ package org.neo4j.rest.graphdb; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; import org.neo4j.rest.graphdb.query.RestCypherQueryEngine; @@ -22,8 +23,8 @@ public class SimpleTransactionTest extends RestTestBase { @Test public void testQueryWithinTransaction() throws Exception { - RestGraphDatabase db = (RestGraphDatabase) getRestGraphDb(); - RestCypherQueryEngine cypher = new RestCypherQueryEngine(db.getRestAPI()); + GraphDatabaseService db = getRestGraphDb(); + RestCypherQueryEngine cypher = new RestCypherQueryEngine(((RestAPIProvider)db).getRestAPI()); Transaction tx = db.beginTx(); QueryResult> result = cypher.query("CREATE (person1 { personId: {id}, started: {started} }) return person1", map("id", 1, "started", System.currentTimeMillis())); diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/query/CypherTransactionTest.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/query/CypherTransactionTest.java index fc19a09bc..6982bbd06 100644 --- a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/query/CypherTransactionTest.java +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/query/CypherTransactionTest.java @@ -12,6 +12,7 @@ import java.util.*; import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; +import static org.neo4j.helpers.collection.MapUtil.map; public class CypherTransactionTest extends RestTestBase { @@ -65,7 +66,19 @@ public class CypherTransactionTest extends RestTestBase { CypherTransaction transaction = new CypherTransaction(SERVER_ROOT_URI, CypherTransaction.ResultType.row); CypherTransaction.Result result = transaction.send("CREATE (n {name:'John'}) RETURN id(n)", null); List commit = transaction.commit(); - assertEquals(0, commit.size()); + assertEquals(1, commit.size()); + Node node = getRestGraphDb().getNodeById(((Number) result.getRows().iterator().next().get(0)).longValue()); + assertEquals("John",node.getProperty("name")); + } + + @Test + public void testWriteCommit() throws Exception { + CypherTransaction transaction = new CypherTransaction(SERVER_ROOT_URI, CypherTransaction.ResultType.row); + CypherTransaction.Result result = transaction.send("CREATE (n {name:'John'}) RETURN id(n) as id", null); + Object id = result.iterator().next().get("id"); + CypherTransaction.Result result2 = transaction.send("MATCH (n) WHERE id(n) = {id} return id(n) as id", map("id",id)); + List commit = transaction.commit(); + assertEquals(1, commit.size()); Node node = getRestGraphDb().getNodeById(((Number) result.getRows().iterator().next().get(0)).longValue()); assertEquals("John",node.getProperty("name")); } diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/query/RestCypherTransactionTest.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/query/RestCypherTransactionTest.java new file mode 100644 index 000000000..63ed371b8 --- /dev/null +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/query/RestCypherTransactionTest.java @@ -0,0 +1,101 @@ +package org.neo4j.rest.graphdb.query; + +import org.junit.Test; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.NotFoundException; +import org.neo4j.graphdb.Transaction; +import org.neo4j.rest.graphdb.*; +import org.neo4j.rest.graphdb.entity.RestEntity; + +import java.util.*; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.neo4j.helpers.collection.MapUtil.map; + +public class RestCypherTransactionTest extends RestTestBase { + + /* + @Test + public void testSingleSend() throws Exception { + CypherTransaction transaction = new CypherTransaction(SERVER_ROOT_URI, CypherTransaction.ResultType.row); + CypherTransaction.Result result = transaction.send("RETURN 42", null); + assertEquals(asList("42"), result.getColumns()); + Iterator> rows = result.getRows().iterator(); + assertEquals(true,rows.hasNext()); + assertEquals(Arrays.asList(42), rows.next()); + assertEquals(false,rows.hasNext()); + assertEquals("RETURN 42", result.getStatement().getStatement()); + assertEquals(Collections.emptyMap(), result.getStatement().getParameters()); + } + + @Test + public void testGraphResult() throws Exception { + CypherTransaction transaction = new CypherTransaction(SERVER_ROOT_URI, CypherTransaction.ResultType.graph); + transaction.add("CREATE (n:Person {name:'Graph'}) RETURN n", null); + List commit = transaction.commit(); + assertEquals(1, commit.size()); + CypherTransaction.Result result = commit.get(0); + Map nodeMap = (Map) result.getRows().iterator().next().get(0); + assertEquals("Graph", ((Map)nodeMap.get("properties")).get("name")); + assertEquals(asList("Person"), nodeMap.get("labels")); + assertEquals(true, nodeMap.containsKey("id")); + + Node node = getRestGraphDb().getNodeById(Long.parseLong(nodeMap.get("id").toString())); + assertEquals("Graph",node.getProperty("name")); + + } + @Test + public void testRestResult() throws Exception { + CypherTransaction transaction = new CypherTransaction(SERVER_ROOT_URI, CypherTransaction.ResultType.rest); + transaction.add("CREATE (n:Person {name:'Rest'}) RETURN n", null); + List commit = transaction.commit(); + assertEquals(1, commit.size()); + CypherTransaction.Result result = commit.get(0); + Map nodeMap = (Map) result.getRows().iterator().next().get(0); + assertEquals("Rest", ((Map)nodeMap.get("data")).get("name")); + assertEquals(true, nodeMap.containsKey("self")); + + Node node = getRestGraphDb().getNodeById(RestEntity.getEntityId(nodeMap.get("self").toString())); + assertEquals("Rest",node.getProperty("name")); + + } + + @Test + public void testCommit() throws Exception { + CypherTransaction transaction = new CypherTransaction(SERVER_ROOT_URI, CypherTransaction.ResultType.row); + CypherTransaction.Result result = transaction.send("CREATE (n {name:'John'}) RETURN id(n)", null); + List commit = transaction.commit(); + assertEquals(1, commit.size()); + Node node = getRestGraphDb().getNodeById(((Number) result.getRows().iterator().next().get(0)).longValue()); + assertEquals("John",node.getProperty("name")); + } +*/ + @Test + public void testWriteCommit() throws Exception { + GraphDatabaseService db = getRestGraphDb(); + RestAPI api = ((RestAPIProvider) getRestGraphDb()).getRestAPI(); + Transaction tx1 = api.beginTx(); + Transaction tx2 = api.beginTx(); + CypherResult result = api.query("CREATE (n {name:'John'}) RETURN id(n) as id", null); + Object id = result.getData().iterator().next().get(0); + CypherResult result2 = api.query("MATCH (n) WHERE id(n) = {id} return id(n) as id", map("id", id)); + Object id2 = result2.getData().iterator().next().get(0); + tx2.success();tx2.close(); + tx1.success();tx1.close(); + Node node = db.getNodeById(((Number) id2).longValue()); + assertEquals("John",node.getProperty("name")); + } + + /* + @Test(expected = NotFoundException.class) + public void testRollback() throws Exception { + CypherTransaction transaction = new CypherTransaction(SERVER_ROOT_URI, CypherTransaction.ResultType.row); + CypherTransaction.Result result = transaction.send("CREATE (n {name:'John'}) RETURN id(n)", null); + transaction.rollback(); + RestAPIImpl api = new RestAPIImpl(SERVER_ROOT_URI); + api.getNodeById(((Number) result.getRows().iterator().next().get(0)).longValue(), RestAPIInternal.Load.ForceFromServer); + } + */ +} diff --git a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestEntityPropertyValidationTests.java b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestEntityPropertyValidationTests.java index 3f8226e71..5f5c99e35 100644 --- a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestEntityPropertyValidationTests.java +++ b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestEntityPropertyValidationTests.java @@ -16,6 +16,7 @@ package org.springframework.data.neo4j.rest.integration; +import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; diff --git a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestGraphRepositoryTests.java b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestGraphRepositoryTests.java index 30293832c..b9ec18822 100644 --- a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestGraphRepositoryTests.java +++ b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestGraphRepositoryTests.java @@ -20,6 +20,7 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.neo4j.rest.graphdb.query.CypherTransactionExecutionException; import org.springframework.data.neo4j.aspects.support.GraphRepositoryTests; import org.springframework.data.neo4j.rest.support.RestTestBase; import org.springframework.test.context.CleanContextCacheTestExecutionListener; @@ -58,13 +59,13 @@ public class RestGraphRepositoryTests extends GraphRepositoryTests { // exception thrown when duplicate violations occur /* Unfortunately the REST scenario does not provide us with enough info to work out that this - was a constaint violation (other than parsing the message itself which is not great) as it + was a constraint violation (other than parsing the message itself which is not great) as it merely throws an IllegalStateException when no content is found for a response when adding labels. The org.neo4j.rest.graphdb.ExecutingRestAPI.addLabels(RestNode node, String...labels) method is the current offender in this case. The REST API itself would probably need to change for us to be able to deal with this appropriately in SDN */ - @Test(expected = IllegalStateException.class) + @Test(expected = CypherTransactionExecutionException.class) public void testSaveWhenFailOnDuplicateSetToTrue() { super.testSaveWhenFailOnDuplicateSetToTrue(); } diff --git a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestIndexTests.java b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestIndexTests.java index 32c58edf2..a5a9ffcc7 100644 --- a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestIndexTests.java +++ b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestIndexTests.java @@ -19,7 +19,9 @@ package org.springframework.data.neo4j.rest.integration; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.data.neo4j.aspects.Person; import org.springframework.data.neo4j.aspects.support.IndexTests; import org.springframework.data.neo4j.rest.support.RestTestBase; import org.springframework.test.annotation.DirtiesContext; @@ -29,6 +31,11 @@ import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.transaction.annotation.Transactional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.springframework.data.neo4j.aspects.Person.persistedPerson; /** * @author mh @@ -56,4 +63,18 @@ public class RestIndexTests extends IndexTests { } + @Test + public void testOutsideRangeQueryPersonByIndexOnAnnotatedField() { + persistedPerson(NAME_VALUE, 35); + Iterable emptyResult = this.personRepository.findAllByRange("age", 0, 34); + assertFalse("nothing found outside range", emptyResult.iterator().hasNext()); + } + + + @Test + public void testRangeQueryPersonByIndexOnAnnotatedField() { + Person person = persistedPerson(NAME_VALUE, 35); + final Person found = this.personRepository.findAllByRange("age", 10, 40).iterator().next(); + assertEquals("person found inside range", person, found); + } } diff --git a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestNodeEntityRelationshipTests.java b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestNodeEntityRelationshipTests.java index 57d0d55b5..0f4765cd2 100644 --- a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestNodeEntityRelationshipTests.java +++ b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestNodeEntityRelationshipTests.java @@ -19,7 +19,16 @@ package org.springframework.data.neo4j.rest.integration; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Test; import org.junit.runner.RunWith; +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.DynamicRelationshipType; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Transaction; +import org.neo4j.helpers.collection.IteratorUtil; +import org.springframework.data.neo4j.aspects.Group; +import org.springframework.data.neo4j.aspects.Mentorship; +import org.springframework.data.neo4j.aspects.Person; import org.springframework.data.neo4j.aspects.support.NodeEntityRelationshipTests; import org.springframework.data.neo4j.rest.support.RestTestBase; import org.springframework.test.context.CleanContextCacheTestExecutionListener; @@ -28,6 +37,10 @@ import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.transaction.annotation.Transactional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; /** * @author mh @@ -55,4 +68,28 @@ public class RestNodeEntityRelationshipTests extends NodeEntityRelationshipTests } + @Test + @Transactional + public void testUpdateSingleRelatedToViaField() { + Group group; + final Long firstMentorshipId; + final Person mentor2; + try (Transaction tx = neo4jTemplate.getGraphDatabaseService().beginTx()) { + group = persist(new Group()); + group.setMentorship(new Mentorship(persist(new Person()), group)); + persist(group); + firstMentorshipId = group.getMentorship().getId(); + mentor2 = new Person(); + group.setMentorship(new Mentorship(persist(mentor2), group)); + persist(group); + tx.success(); + } + final Node node = neo4jTemplate.getPersistentState(group); + assertEquals(1, IteratorUtil.count(node.getRelationships(Direction.INCOMING, DynamicRelationshipType.withName("mentors")))); + final Group loaded = neo4jTemplate.load(node, Group.class); + assertFalse(loaded.getMentorship().getId().equals(firstMentorshipId)); + assertEquals(mentor2, group.getMentorship().getMentor()); + assertEquals(group, group.getMentorship().getGroup()); + } + } diff --git a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestNodeEntityTests.java b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestNodeEntityTests.java index b11498373..1b0c66876 100644 --- a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestNodeEntityTests.java +++ b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/integration/RestNodeEntityTests.java @@ -19,6 +19,7 @@ package org.springframework.data.neo4j.rest.integration; import org.junit.*; import org.junit.runner.RunWith; import org.neo4j.graphdb.ConstraintViolationException; +import org.neo4j.rest.graphdb.query.CypherTransactionExecutionException; import org.springframework.data.neo4j.aspects.support.NodeEntityTests; import org.springframework.data.neo4j.rest.support.RestTestBase; import org.springframework.test.context.CleanContextCacheTestExecutionListener; @@ -59,9 +60,7 @@ public class RestNodeEntityTests extends NodeEntityTests { // super.testSetShortProperty(); } - // TODO - Change REST to have a better (more descriptive) - // exception thrown when duplicate violations occur - @Test(expected = IllegalStateException.class) + @Test(expected = CypherTransactionExecutionException.class) public void testDefaultFailOnDuplicateSetToTrueCausesExceptionWhenAnotherDuplicateEntityCreated() { super.testDefaultFailOnDuplicateSetToTrueCausesExceptionWhenAnotherDuplicateEntityCreated(); } diff --git a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestQueryEngineTests.java b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestQueryEngineTests.java index 751006162..8fa622480 100644 --- a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestQueryEngineTests.java +++ b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestQueryEngineTests.java @@ -24,6 +24,7 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.neo4j.aspects.support.query.QueryEngineTests; import org.springframework.data.neo4j.core.GraphDatabase; +import org.springframework.data.neo4j.rest.SpringCypherRestGraphDatabase; import org.springframework.data.neo4j.rest.SpringRestGraphDatabase; import org.springframework.test.context.CleanContextCacheTestExecutionListener; import org.springframework.test.context.ContextConfiguration; @@ -44,7 +45,7 @@ import org.springframework.test.context.transaction.TransactionalTestExecutionLi public class RestQueryEngineTests extends QueryEngineTests { @Autowired - SpringRestGraphDatabase restGraphDatabase; + SpringCypherRestGraphDatabase restGraphDatabase; @BeforeClass public static void startDb() throws Exception { @@ -71,4 +72,4 @@ public class RestQueryEngineTests extends QueryEngineTests { restGraphDatabase.setConversionService(conversionService); return restGraphDatabase; } -} \ No newline at end of file +} diff --git a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestTestBase.java b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestTestBase.java index cff5233e8..ce758466b 100644 --- a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestTestBase.java +++ b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestTestBase.java @@ -17,6 +17,7 @@ package org.springframework.data.neo4j.rest.support; +import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -87,6 +88,11 @@ public class RestTestBase { refNode = createNode(); } + @After + public void tearDown() throws Exception { + if (restGraphDatabase!=null) restGraphDatabase.shutdown(); + } + public Node createNode() { return restGraphDatabase.createNode(null,null); } diff --git a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestTestHelper.java b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestTestHelper.java index 424f83e4c..c1fccf9d0 100644 --- a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestTestHelper.java +++ b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestTestHelper.java @@ -25,6 +25,7 @@ import org.neo4j.server.configuration.Configurator; import org.neo4j.server.configuration.ServerConfigurator; import org.neo4j.test.ImpermanentGraphDatabase; import org.springframework.data.neo4j.core.GraphDatabase; +import org.springframework.data.neo4j.rest.SpringCypherRestGraphDatabase; import org.springframework.data.neo4j.rest.SpringRestGraphDatabase; public class RestTestHelper @@ -46,7 +47,7 @@ public class RestTestHelper } public GraphDatabase createGraphDatabase() throws URISyntaxException { - return new SpringRestGraphDatabase(SERVER_ROOT_URI); + return new SpringCypherRestGraphDatabase(SERVER_ROOT_URI); } public void cleanDb() { diff --git a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/ServerPluginTests.java b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/ServerPluginTests.java index 9abd022aa..3a93ea502 100644 --- a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/ServerPluginTests.java +++ b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/ServerPluginTests.java @@ -21,6 +21,7 @@ import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.WebResource; import org.codehaus.jackson.map.ObjectMapper; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; import org.neo4j.rest.graphdb.RequestResult; import org.springframework.data.neo4j.aspects.Person; diff --git a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/SpringPluginInitializerTests.java b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/SpringPluginInitializerTests.java index e5a34066b..953df212d 100644 --- a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/SpringPluginInitializerTests.java +++ b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/SpringPluginInitializerTests.java @@ -19,10 +19,7 @@ package org.springframework.data.neo4j.rest.support; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientResponse; import org.apache.commons.configuration.Configuration; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; +import org.junit.*; import org.neo4j.rest.graphdb.RequestResult; import org.neo4j.server.NeoServer; import org.neo4j.server.WrappingNeoServerBootstrapper; diff --git a/spring-data-neo4j-rest/src/test/resources/RestTests-context.xml b/spring-data-neo4j-rest/src/test/resources/RestTests-context.xml index b943b99e0..410731b48 100644 --- a/spring-data-neo4j-rest/src/test/resources/RestTests-context.xml +++ b/spring-data-neo4j-rest/src/test/resources/RestTests-context.xml @@ -3,7 +3,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> - + diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/conversion/DefaultConverter.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/conversion/DefaultConverter.java index 0c0ddc9db..ffa5cc9fe 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/conversion/DefaultConverter.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/conversion/DefaultConverter.java @@ -43,7 +43,7 @@ public class DefaultConverter implements ResultConverter { final Class sourceType = singleValue.getClass(); Object result = doConvert(singleValue, sourceType, type,mappingPolicy); if (result == null) - throw new RuntimeException("Cannot automatically convert " + sourceType + " to " + type + " please use a custom converter"); + throw new RuntimeException("Cannot automatically convert " + sourceType + " to " + type + " please use a custom converter, value: "+value); return (R) result; } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/template/Neo4jTemplateApiTests.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/template/Neo4jTemplateApiTests.java index c60af2d6f..4c5573e8c 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/template/Neo4jTemplateApiTests.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/template/Neo4jTemplateApiTests.java @@ -103,11 +103,19 @@ public class Neo4jTemplateApiTests { @After public void tearDown() throws Exception { if (transaction!=null) { - transaction.success(); - transaction.finish(); + try { + transaction.success(); + transaction.close(); + } catch(Exception e) { + //ignore + } } if (graphDatabaseService!=null) { graphDatabaseService.shutdown(); + } else { + if (graphDatabase != null ) { + graphDatabase.shutdown(); + } } } @@ -174,7 +182,7 @@ public class Neo4jTemplateApiTests { @Test public void shouldFindNextNodeViaCypher() throws Exception { - assertSingleResult(node1, template.query("start n=node(" + node0.getId() + ") match n-->m return m", null).to(Node.class)); + assertSingleResult(node1, template.query("start n=node(" + node0.getId() + ") match (n)-->(m) return m", null).to(Node.class)); } @Test diff --git a/src/docbkx/reference/heroku.xml b/src/docbkx/reference/heroku.xml index 54d1d475a..089fd722a 100644 --- a/src/docbkx/reference/heroku.xml +++ b/src/docbkx/reference/heroku.xml @@ -144,7 +144,7 @@ public class Main { + class="org.springframework.data.neo4j.rest.SpringCypherRestGraphDatabase"> diff --git a/src/main/asciidoc/reference/neo4j-server.adoc b/src/main/asciidoc/reference/neo4j-server.adoc index a0149d524..f663e71c0 100644 --- a/src/main/asciidoc/reference/neo4j-server.adoc +++ b/src/main/asciidoc/reference/neo4j-server.adoc @@ -1,17 +1,26 @@ [[reference_neo4j-server]] = Neo4j Server -Neo4j is not only available in embedded mode. It can also be installed and run as a stand-alone server accessible via a REST API. Developers can integrate Spring Data Neo4j into the Neo4j server infrastructure in two ways: in an unmanaged server extension, or via the REST API. +Neo4j is not only available in embedded mode. It can also be installed and run as a stand-alone server accessible via a HTTP API. +Developers can integrate Spring Data Neo4j into the Neo4j server infrastructure in two ways: as a server extension, or remotely via the HTTP API. + +Spring Data Neo4j was historically built around the Neo4j embedded Java APIs, but are not optimized for remote usage. + Most of the Graph Database operations are sent as Cypher statements to the server's transactional Cypher endpoint. + Only the few operations that are not supported by Cypher yet (legacy indexes, traversals, management operations) use the Neo4j Server REST API. == Server Extension -When should you write a server extension? The default REST API is essentially a REST'ified representation of the Neo4j core API. It is nice for getting started, and for simpler scenarios. For more involved solutions that require high-volume access or more complex operations, writing a server extension that is able to process external parameters, do all the computations locally in the plugin, and then return just the relevant information to the calling client is preferable. +When would you write a server extension? + If you want to achieve the performance, that you get from the embedded Neo4j usage in Spring Data Neo4j, then a server extension is the easiest way. + Running as a extension within Neo4j Server, Spring Data Neo4j can access the Neo4j the same way as running with a embedded database inside your Spring application. -The Neo4j Server has two built-in extension mechanisms. It is possible to extend existing URI endpoints like the graph database, nodes, or relationships, adding new URIs or methods to those. This is achieved by writing a http://neo4j.com/docs/milestone/server-plugins.html[server plugin]. This plugin type has some restrictions however. +The Neo4j Server has two built-in extension mechanisms. It is possible to extend existing REST endpoints for the graph database, nodes, or relationships, adding new service URIs or methods to those. + This is achieved by writing a http://neo4j.com/docs/stable/server-plugins.html[server plugin]. However this approach has some restrictions in terms of HTTP verbs and result types. -For complete freedom in the implementation, an http://neo4j.com/docs/milestone/server-unmanaged-extensions.html[unmanaged extension] can be used. Unmanaged extensions are essentially http://jersey.java.net/[Jersey] resource implementations. The resource constructors or methods can get the `GraphDatabaseService` injected to execute the necessary operations and return appropriate `Representations`. +For an unrestricted implementation, an http://neo4j.com/docs/stable/server-unmanaged-extensions.html[unmanaged extension] can be used. Unmanaged extensions are essentially http://jersey.java.net/[Jersey] resource implementations. +The resource constructors or methods can get `@Context GraphDatabaseService` and `@Context CypherExecutor` instances injected to run the necessary Neo4j API calls and Cypher statements and return appropriate `Representations`. -Both kinds of extensions have to be packaged as JAR files and added to the Neo4j Server's plugin directory. Server Plugins are picked up by the server at startup if they provide the necessary `META-INF.services/org.neo4j.server.plugins.ServerPlugin` file for Java's ServiceLoader facility. Unmanaged extensions have to be registered with the Neo4j Server configuration. +Both kinds of extensions have to be packaged as JAR files and added to the Neo4j Server's plugin directory. Server Plugins are picked up by the server at startup if they provide the necessary `META-INF.services/org.neo4j.server.plugins.ServerPlugin` file for Java's ServiceLoader facility. Unmanaged extensions have to be registered with the Neo4j Server configuration in `conf/neo4j-wrapper.conf`. .Configuring an unmanaged extension ==== @@ -21,7 +30,8 @@ org.neo4j.server.thirdparty_jaxrs_classes=com.example.mypackage=/my-context ---- ==== -Running Spring Data Neo4j on the Neo4j Server is easy. You need to tell the server where to find the Spring context configuration file, and which beans from it to expose: +Integrating Spring Data Neo4j ApplicationContext configuration in the Neo4j Server is easy. +You provide the Spring context configuration location, and list which Spring-beans should be exposed: .Server plugin initialization ==== @@ -37,7 +47,7 @@ public class HelloWorldInitializer extends SpringPluginInitializer { ---- ==== -Now, your resources can require the Spring beans they need, annotated with `@Context` like this: +Now, your resources can require the Spring beans they need as parameters, annotated with `@Context`: .Jersey resource ==== @@ -52,41 +62,57 @@ public void foo( @Context WorldRepository repo ) { ---- ==== -The `SpringPluginInitializer` merges the server provided `GraphDatabaseService` with the Spring configuration and registers the named beans as Jersey `Injectables`. It is still necessary to list the initializer's fully qualified class name in a file named `META-INF/services/org.neo4j.server.plugins.PluginLifecycle`. The Neo4j Server can then pick up and run the initialization classes before the extensions are loaded. +The `SpringPluginInitializer` merges the server provided `GraphDatabaseService` with the Spring configuration and registers the named beans as Jersey `Injectables`. + It is still necessary to list the `SpringPluginInitializer` implementation's fully qualified class name in a file named `META-INF/services/org.neo4j.server.plugins.PluginLifecycle`, e.g. `org.example.extension.HelloWorldInitializer`. + The Neo4j Server can then pick up and run the initialization classes before the extensions are loaded. -== Using Spring Data Neo4j as a REST client +== Using Spring Data Neo4j as a Neo4j Server client -To use REST-API the Neo4j Server exposes, one would either go with REST libraries on the lower level or choose one of the Neo4j related REST drivers in various languages. For Java Neo4j provides the https://github.com/neo4j/java-rest-binding[Neo4j Java REST bindings] which come as a drop in replacement for the `GraphDatabaseService` API. Spring Data Neo4j REST uses those bindings to provide seamless access to a remote Neo4j Database. +To use Neo4j's remote APIs, you can use them directly to send Cypher statements to the server, e.g. with the http://neo4j.com/developer/java/#_using_spring_boot_with_jdbc[Neo4j-JDBC] driver. + That JDBC driver integrates well with the commonly used `spring-jdbc` libraries and classes. -By simply configuring the `graphDatabaseService` to be a `SpringRestGraphDatabase` pointing to a Neo4j Server instance and referring to that from `` Spring Data Neo4j will use the server side database for both the simple mapping as well as the advanced mapping. +There are also other http://neo4j.com/developer/language-guides[remote drivers] for Neo4j available. -NOTE: The Neo4j Server REST API does not allow for transactions to span across requests, which means that Spring Data Neo4j is not transactional across multiple operations when running with a `SpringRestGraphDatabase`. +Spring Data Neo4j's integration with the server also uses the Cypher endpoint to execute GraphDatabaseService operations transactionally against the server. + The implementation of the integration is handled by `SpringCypherRestGraphDatabase` and `RestAPICypherImpl` which wraps the _old_ REST-API methods in the appropriate Cypher statement calls. + It integrates with the Spring Transaction APIs by providing a `javax.transaction.TransactionManager` implementation that is configured to be used by the `JtaTransactionManager` bean provided by Spring Data Neo4j. -Please also keep in mind that performing graph operations via the REST-API is about one order of magnitude slower than local operations. Try to use the Neo4j Cypher query language, or server-side traversals (`RestTraversal`) whenever possible for retrieving large sets of data. Future versions of Spring Data Neo4j will use the more performant batch API as well as a binary protocol. +By simply configuring the `graphDatabaseService` to be a `SpringCypherRestGraphDatabase` pointing to a Neo4j Server instance and referring to that from ``, Spring Data Neo4j will use the server side database for both the simple mapping as well as the advanced mapping. -To set up your project to use the REST bindings, add this dependency to your pom.xml: +NOTE: The Neo4j Server REST API does not allow for transactions to span across requests, which means that all operations that are not handled by Cypher (traversals, legacy index lookups and management operations) are not participating in the Cypher transactions. -.REST-Client configuration - pom.xml +Please also keep in mind that performing graph operations via the remote API is slower than local operations. + You have to take roundtrip latency, request serialization and result parsing into account. + Also remember that Spring Data Neo4j was built around Neo4j's embedded APIs, that's why it is not the most efficient user of the remote API. + A new version of a Java OGM and Spring Data Neo4j is in development that addresses these issues. + +If the mapping CRUD operations are too slow, try to avoid the automatic fetching of additional levels of entities. +Basic CRUD should be fast enough. +Use Cypher to execute operations within the server and map the results using `@QueryResult` POJOs or interfaces. + +To set up your project to use the remote Neo4j Server integration, add this dependency to your `pom.xml`: + +.Remote Client configuration - pom.xml ==== [source,xml] ---- org.springframework.data spring-data-neo4j-rest - 2.1.0.RELEASE + 3.3.0.M1 ---- ==== -Now, you set up the normal Spring Data Neo4j configuration, but point the database to an URL instead of a local directory, like so: +Now, you set up the normal Spring Data Neo4j configuration, but point the database instance to an URL instead of a local directory: -.REST client configuration - application context +.Remote configuration - application context ==== [source,xml] ---- - +