diff --git a/spring-data-neo4j-cross-store/src/main/java/org/springframework/data/neo4j/cross_store/config/CrossStoreNeo4jConfiguration.java b/spring-data-neo4j-cross-store/src/main/java/org/springframework/data/neo4j/cross_store/config/CrossStoreNeo4jConfiguration.java index 5ab08faa3..57b219b2a 100644 --- a/spring-data-neo4j-cross-store/src/main/java/org/springframework/data/neo4j/cross_store/config/CrossStoreNeo4jConfiguration.java +++ b/spring-data-neo4j-cross-store/src/main/java/org/springframework/data/neo4j/cross_store/config/CrossStoreNeo4jConfiguration.java @@ -15,11 +15,13 @@ */ package org.springframework.data.neo4j.cross_store.config; +import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Node; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; import org.springframework.data.neo4j.aspects.config.Neo4jAspectConfiguration; import org.springframework.data.neo4j.config.JtaTransactionManagerFactoryBean; import org.springframework.data.neo4j.cross_store.support.node.CrossStoreNodeDelegatingFieldAccessorFactory; @@ -69,8 +71,8 @@ public class CrossStoreNeo4jConfiguration extends Neo4jAspectConfiguration { } @Bean - public PlatformTransactionManager neo4jTransactionManager() throws Exception { - JtaTransactionManager jtaTm = new JtaTransactionManagerFactoryBean( getGraphDatabaseService() ).getObject(); + public PlatformTransactionManager neo4jTransactionManager(GraphDatabaseService graphDatabaseService) { + JtaTransactionManager jtaTm = new JtaTransactionManagerFactoryBean( graphDatabaseService ).getObject(); if (isUsingCrossStorePersistence()) { JpaTransactionManager jpaTm = new JpaTransactionManager(getEntityManagerFactory()); 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 new file mode 100644 index 000000000..6ea295d97 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/CypherRestGraphDatabase.java @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2002-2013 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.rest.graphdb; + + +import org.neo4j.graphdb.*; +import org.neo4j.graphdb.schema.Schema; +import org.neo4j.graphdb.traversal.BidirectionalTraversalDescription; +import org.neo4j.kernel.impl.nioneo.store.StoreId; +import org.neo4j.rest.graphdb.entity.RestNode; +import org.neo4j.rest.graphdb.index.RestIndexManager; +import org.neo4j.rest.graphdb.query.RestCypherTransactionManager; +import org.neo4j.rest.graphdb.traversal.RestTraversalDescription; +import org.neo4j.rest.graphdb.util.ResourceIterableWrapper; + +import javax.transaction.SystemException; +import javax.transaction.TransactionManager; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + + +public class CypherRestGraphDatabase extends AbstractRemoteDatabase { + private RestAPICypherImpl restAPI; + + public CypherRestGraphDatabase(RestAPI restAPI){ + this.restAPI = new RestAPICypherImpl(restAPI); + } + + public CypherRestGraphDatabase(String uri) { + this( new RestAPIImpl( uri )); + } + + public CypherRestGraphDatabase(String uri, String user, String password) { + this(new RestAPIImpl( uri, user, password )); + } + + public RestAPI getRestAPI(){ + return this.restAPI; + } + + public RestIndexManager index() { + return this.restAPI.index(); + } + + public Node createNode() { + return this.restAPI.createNode(null); + } + + public Node getNodeById( long id ) { + return this.restAPI.getNodeById(id); + } + + @Override + public Iterable getAllNodes() { + return restAPI.getAllNodes(); + } + + @Override + public Iterable getRelationshipTypes() { + return this.restAPI.getRelationshipTypes(); + } + + public Relationship getRelationshipById( long id ) { + return this.restAPI.getRelationshipById(id); + } + @Override + public String getStoreDir() { + return restAPI.getBaseUri(); + } + + @Override + public StoreId storeId() { + return null; + } + + @Override + public boolean isAvailable(long timeout) { + return restAPI!=null; + } + + public RestCypherTransactionManager getTxManager() { + return restAPI.getTxManager(); + } + + @Override + public DependencyResolver getDependencyResolver() { + return new DependencyResolver.Adapter() { + @Override + public T resolveDependency(Class type, SelectionStrategy selector) throws IllegalArgumentException { + if (TransactionManager.class.isAssignableFrom(type)) return (T)getTxManager(); + return null; + } + }; + } + + @Override + public Transaction beginTx() { + return restAPI.beginTx(); + } + + @Override + public void shutdown() { + try { + getTxManager().rollback(); + } catch (SystemException e) { + // ignore + } + restAPI.close(); + } + + @Override + public Node createNode(Label... labels) { + return restAPI.createNode(null, toLabelNames(labels)); + } + + public Set toLabelNames(Label[] labels) { + Set labelNames = new LinkedHashSet<>(labels.length); + for (Label label : labels) labelNames.add(label.name()); + return labelNames; + } + + @Override + public ResourceIterable findNodesByLabelAndProperty(Label label, String property, Object value) { + Iterable nodes = restAPI.getNodesByLabelAndProperty(label.name(), property, value); + return new ResourceIterableWrapper(nodes) { + protected Node underlyingObjectToObject(RestNode node) { + return node; + } + }; + } + + @Override + public Schema schema() { + throw new UnsupportedOperationException(); + } + + @Override + public RestTraversalDescription traversalDescription() { + return restAPI.createTraversalDescription(); + } + + @Override + public BidirectionalTraversalDescription bidirectionalTraversalDescription() { + throw new UnsupportedOperationException(); + } + + public Collection getAllLabelNames() { + return restAPI.getAllLabelNames(); + } +} + diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPI.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPI.java index 120192a0d..4bb3a2da3 100644 --- a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPI.java +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPI.java @@ -25,6 +25,7 @@ import org.neo4j.rest.graphdb.query.CypherRestResult; import org.neo4j.rest.graphdb.entity.RestEntity; import org.neo4j.rest.graphdb.entity.RestNode; import org.neo4j.rest.graphdb.entity.RestRelationship; +import org.neo4j.rest.graphdb.traversal.RestTraversalDescription; import org.neo4j.rest.graphdb.traversal.RestTraverser; import org.neo4j.rest.graphdb.util.QueryResult; import org.neo4j.rest.graphdb.util.ResultConverter; @@ -71,7 +72,7 @@ public interface RestAPI extends RestAPIIndex, RestAPIInternal { Iterable getRelationshipTypes(); - TraversalDescription createTraversalDescription(); + RestTraversalDescription createTraversalDescription(); Iterable getRelationships(RestNode restNode, Direction direction, RelationshipType... types); @@ -87,6 +88,7 @@ public interface RestAPI extends RestAPIIndex, RestAPIInternal { RestNode addToCache(RestNode restNode); RestNode getFromCache(long id); + void removeFromCache(long id); void 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 b5f16f5c5..55811c4fb 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 @@ -33,13 +33,12 @@ import org.neo4j.rest.graphdb.entity.RestRelationship; import org.neo4j.rest.graphdb.index.IndexInfo; import org.neo4j.rest.graphdb.index.RestIndex; import org.neo4j.rest.graphdb.index.RestIndexManager; -import org.neo4j.rest.graphdb.query.CypherResult; -import org.neo4j.rest.graphdb.query.CypherTransaction; -import org.neo4j.rest.graphdb.query.CypherTxResult; -import org.neo4j.rest.graphdb.query.RestQueryResult; +import org.neo4j.rest.graphdb.query.*; import org.neo4j.rest.graphdb.transaction.RemoteCypherTransaction; +import org.neo4j.rest.graphdb.traversal.RestTraversalDescription; import org.neo4j.rest.graphdb.traversal.RestTraverser; 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; @@ -52,7 +51,11 @@ 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 String MATCH_NODE_QUERY(String name) { return " MATCH ("+name+") WHERE id("+name+") = {id_"+name+"} "; } + + public static String MATCH_NODE_QUERY(String name) { + return " MATCH (" + name + ") WHERE id(" + name + ") = {id_" + name + "} "; + } + public static final String _MATCH_NODE_QUERY = " MATCH (n) WHERE id(n) = {id} "; public static final String GET_NODE_QUERY = _MATCH_NODE_QUERY + _QUERY_RETURN_NODE; public static final String _MATCH_REL_QUERY = " START r=rel({id}) "; @@ -67,17 +70,17 @@ public class RestAPICypherImpl implements RestAPI { private String mergeQuery(String labelName, String key, Collection labels) { StringBuilder setLabels = new StringBuilder(); - if (labels!=null) { + if (labels != null) { for (String label : labels) { if (label.equals(labelName)) continue; setLabels.append("SET n:").append(label).append(" "); } } - return "MERGE (n:`"+labelName+"` {`"+key+"`: {value}}) ON CREATE SET n={props} "+setLabels+ _QUERY_RETURN_NODE; + return "MERGE (n:`" + labelName + "` {`" + key + "`: {value}}) ON CREATE SET n={props} " + setLabels + _QUERY_RETURN_NODE; } private String toLabelString(Collection labels) { - if (labels==null || labels.size() == 0) return ""; + if (labels == null || labels.size() == 0) return ""; StringBuilder sb = new StringBuilder(); for (String label : labels) { sb.append(":").append(label); @@ -86,13 +89,11 @@ public class RestAPICypherImpl implements RestAPI { } private final RestAPI restAPI; - private RestRequest restRequest; - private static final ThreadLocal cypherTransaction = new ThreadLocal<>(); + private final RestCypherTransactionManager txManager = new RestCypherTransactionManager(this); protected RestAPICypherImpl(RestAPI restAPI) { this.restAPI = restAPI; - this.restRequest = restAPI.getRestRequest(); } @Override @@ -101,7 +102,7 @@ public class RestAPICypherImpl implements RestAPI { RestNode restNode = getFromCache(id); if (restNode != null) return restNode; } - if (force == Load.FromCache) return new RestNode(RestNode.nodeUri(this, id),this); + if (force == Load.FromCache) return new RestNode(RestNode.nodeUri(this, id), this); Iterator> result = query(GET_NODE_QUERY, map("id", id)).getData().iterator(); if (!result.hasNext()) { throw new NotFoundException("Node not found " + id); @@ -114,6 +115,11 @@ public class RestAPICypherImpl implements RestAPI { return restAPI.getFromCache(id); } + @Override + public void removeFromCache(long id) { + restAPI.removeFromCache(id); + } + @Override public RestNode getNodeById(long id) { return getNodeById(id, Load.FromServer); @@ -122,34 +128,44 @@ public class RestAPICypherImpl implements RestAPI { private RestNode toNode(List row) { long id = ((Number) row.get(0)).longValue(); List labels = (List) row.get(1); - Map props = (Map) row.get(2); + Map props = (Map) row.get(2); return RestNode.fromCypher(id, labels, props, this); } private RestRelationship toRel(List row) { long id = ((Number) row.get(0)).longValue(); - String type = (String)row.get(1); - Map props = (Map) row.get(2); + String type = (String) row.get(1); + Map props = (Map) row.get(2); long start = ((Number) row.get(3)).longValue(); long end = ((Number) row.get(4)).longValue(); - return RestRelationship.fromCypher(id, type, props, start,end,this); + return RestRelationship.fromCypher(id, type, props, start, end, this); } @Override public RestRelationship getRelationshipById(long id) { - Iterator> result = query(GET_REL_QUERY, map("id", id)).getData().iterator(); - if (!result.hasNext()) { - throw new NotFoundException("Relationship not found " + id); + try { + Iterator> result = query(GET_REL_QUERY, map("id", id)).getData().iterator(); + if (!result.hasNext()) { + throw new NotFoundException("Relationship not found " + id); + } + List row = result.next(); + return toRel(row); + } catch (NotFoundException e) { + throw e; + } catch (CypherTransactionExecutionException ctee) { + if (ctee.contains("Neo.DatabaseError.Statement.ExecutionFailure","not found")) { + throw new NotFoundException("Relationship not found " + id); + } + throw ctee; } - List row = result.next(); - return toRel(row); } @Override public RestNode createNode(Map props) { - return createNode(props,Collections.emptyList()); + return createNode(props, Collections.emptyList()); } + @Override public RestNode createNode(Map props, Collection labels) { Map data = props == null ? Collections.emptyMap() : props; @@ -162,12 +178,13 @@ public class RestAPICypherImpl implements RestAPI { @Override public RestNode merge(String labelName, String key, Object value, final Map nodeProperties, Collection labels) { - if (labelName ==null || key == null || value==null) throw new IllegalArgumentException("Label "+ labelName +" key "+key+" and value must not be null"); + if (labelName == null || key == null || value == null) + throw new IllegalArgumentException("Label " + labelName + " key " + key + " and value must not be null"); 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(); if (!result.hasNext()) - throw new RuntimeException("Error merging node with labels: " + labelName + " key " + key + " value " + value + " labels " + labels+ " and props: " + props + " no data returned"); + throw new RuntimeException("Error merging node with labels: " + labelName + " key " + key + " value " + value + " labels " + labels + " and props: " + props + " no data returned"); return addToCache(toNode(result.next())); } @@ -178,10 +195,11 @@ public class RestAPICypherImpl implements RestAPI { @Override public RestRelationship createRelationship(Node startNode, Node endNode, RelationshipType type, Map props) { - String statement = MATCH_NODE_QUERY("n") + MATCH_NODE_QUERY("m") + " CREATE (n)-[r:`"+type.name()+"`]->(m) SET r={props} " + _QUERY_RETURN_REL; - Map params = map("id_n", startNode.getId(), "id_m", endNode.getId(), "props", props); + String statement = MATCH_NODE_QUERY("n") + MATCH_NODE_QUERY("m") + " CREATE (n)-[r:`" + type.name() + "`]->(m) SET r={props} " + _QUERY_RETURN_REL; + Map params = map("id_n", startNode.getId(), "id_m", endNode.getId(), "props", props == null ? Collections.emptyMap() : props); CypherTransaction.Result result = runQuery(statement, params); - if (!result.hasData()) throw new RuntimeException("Error creating relationship from "+startNode+" to "+endNode+" type "+type.name()); + if (!result.hasData()) + throw new RuntimeException("Error creating relationship from " + startNode + " to " + endNode + " type " + type.name()); Iterator> it = result.getRows().iterator(); return toRel(it.next()); } @@ -191,7 +209,7 @@ public class RestAPICypherImpl implements RestAPI { public void removeLabel(RestNode node, String label) { CypherTransaction.Result result = runQuery(_MATCH_NODE_QUERY + (" REMOVE n:`" + label + "` ") + _QUERY_RETURN_NODE, map("id", node.getId())); if (!result.hasData()) { - throw new RuntimeException("Error removing label "+label+" from node "+node); + throw new RuntimeException("Error removing label " + label + " from node " + node); } } @@ -203,7 +221,7 @@ public class RestAPICypherImpl implements RestAPI { private Iterable queryForNodes(String statement, Map params) { Iterable> result = runQuery(statement, params).getRows(); - return new IterableWrapper>(result) { + return new IterableWrapper>(result) { protected RestNode underlyingObjectToObject(List row) { return addToCache(toNode(row)); } @@ -212,7 +230,7 @@ public class RestAPICypherImpl implements RestAPI { @Override public Iterable getNodesByLabelAndProperty(String label, String property, Object value) { - String statement = "MATCH (n:`" + label + "`) WHERE n.`"+property+"` = {value} " + _QUERY_RETURN_NODE; + String statement = "MATCH (n:`" + label + "`) WHERE n.`" + property + "` = {value} " + _QUERY_RETURN_NODE; return queryForNodes(statement, map("value", value)); } @@ -228,16 +246,16 @@ public class RestAPICypherImpl implements RestAPI { @Override public int getDegree(RestNode restNode, RelationshipType type, Direction direction) { - String nodeDegreeQuery = "MATCH (n)" + relPattern(direction,type) + "() WHERE id(n) = {id} RETURN count(*) as degree"; + String nodeDegreeQuery = "MATCH (n)" + relPattern(direction, type) + "() WHERE id(n) = {id} RETURN count(*) as degree"; Iterator> degree = runQuery(nodeDegreeQuery, map("id", restNode.getId())).getRows().iterator(); if (!degree.hasNext()) return 0; - return ((Number)degree.next().get(0)).intValue(); + return ((Number) degree.next().get(0)).intValue(); } private String relPattern(Direction direction, RelationshipType... types) { String typeString = toTypeString(types); - String relPattern = "--"; - if (!typeString.isEmpty()) relPattern = "-[r "+typeString+"]-"; + String relPattern = "-[r]-"; + if (!typeString.isEmpty()) relPattern = "-[r " + typeString + "]-"; if (direction == Direction.OUTGOING) { relPattern += ">"; } else if (direction == Direction.INCOMING) { @@ -247,10 +265,10 @@ public class RestAPICypherImpl implements RestAPI { } private String toTypeString(RelationshipType... types) { - if (types==null || types.length == 0) return ""; + if (types == null || types.length == 0) return ""; StringBuilder typeString = new StringBuilder(); for (RelationshipType type : types) { - if (typeString.length() > 0 ) typeString.append("|"); + if (typeString.length() > 0) typeString.append("|"); typeString.append(':').append('`').append(type.name()).append("`"); } return typeString.toString(); @@ -258,9 +276,9 @@ public class RestAPICypherImpl implements RestAPI { @Override public Iterable getRelationships(RestNode restNode, Direction direction, RelationshipType... types) { - String statement = _MATCH_NODE_QUERY + " MATCH (n)"+relPattern(direction,types)+"() "+_QUERY_RETURN_REL; + String statement = _MATCH_NODE_QUERY + " MATCH (n)" + relPattern(direction, types) + "() " + _QUERY_RETURN_REL; CypherTransaction.Result result = runQuery(statement, map("id", restNode.getId())); - return new IterableWrapper>(result.getRows()) { + return new IterableWrapper>(result.getRows()) { protected Relationship underlyingObjectToObject(List row) { return toRel(row); } @@ -269,8 +287,8 @@ 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())); + 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); if (response.statusOtherThan(Status.NO_CONTENT)) { @@ -279,50 +297,48 @@ public class RestAPICypherImpl implements RestAPI { } public RestRequest getRestRequest() { - return restRequest; + return restAPI.getRestRequest(); } @Override public Transaction beginTx() { - CypherTransaction tx = cypherTransaction.get(); - if (tx != null ) { - throw new IllegalStateException("Transaction already running "+tx); - } else { - cypherTransaction.set(newCypherTransaction()); - return new RemoteCypherTransaction(cypherTransaction); - } + return txManager.beginTx(); + } + + public RestCypherTransactionManager getTxManager() { + return txManager; } @Override public IndexHits getIndex(Class entityType, String indexName, String key, Object value) { String index = key == null ? ":`" + indexName + "`({query})" : ":`" + indexName + "`(`" + key + "`={query})"; if (Node.class.isAssignableFrom(entityType)) { - String statement = "start n=node"+index+ _QUERY_RETURN_NODE; + String statement = "start n=node" + index + _QUERY_RETURN_NODE; CypherTransaction.Result result = runQuery(statement, map("query", value)); - return toIndexHits(result,true); + return toIndexHits(result, true); } if (Relationship.class.isAssignableFrom(entityType)) { - String statement = "start r=rel"+index+ _QUERY_RETURN_REL; + String statement = "start r=rel" + index + _QUERY_RETURN_REL; CypherTransaction.Result result = runQuery(statement, map("query", value)); - return toIndexHits(result,false); + return toIndexHits(result, false); } - throw new IllegalStateException("Unknown index entity type "+entityType); + throw new IllegalStateException("Unknown index entity type " + entityType); } @Override public IndexHits queryIndex(Class entityType, String indexName, String key, Object value) { - String index = ":`" + indexName + "`({query})"; + String index = ":`" + indexName + "`({query})"; if (Node.class.isAssignableFrom(entityType)) { - String statement = "start n=node"+index+ _QUERY_RETURN_NODE; + String statement = "start n=node" + index + _QUERY_RETURN_NODE; CypherTransaction.Result result = runQuery(statement, map("query", value)); - return toIndexHits(result,true); + return toIndexHits(result, true); } if (Relationship.class.isAssignableFrom(entityType)) { - String statement = "start r=rel"+index+ _QUERY_RETURN_REL; + String statement = "start r=rel" + index + _QUERY_RETURN_REL; CypherTransaction.Result result = runQuery(statement, map("query", value)); - return toIndexHits(result,false); + return toIndexHits(result, false); } - throw new IllegalStateException("Unknown index entity type "+entityType); + throw new IllegalStateException("Unknown index entity type " + entityType); } private IndexHits toIndexHits(CypherTransaction.Result result, final boolean isNode) { @@ -342,7 +358,7 @@ public class RestAPICypherImpl implements RestAPI { @Override protected S fetchNextOrNull() { if (!it.hasNext()) return null; - return (S)(isNode ? addToCache(toNode(it.next())) : toRel(it.next())); + return (S) (isNode ? addToCache(toNode(it.next())) : toRel(it.next())); } }; } @@ -357,17 +373,18 @@ public class RestAPICypherImpl implements RestAPI { public void deleteEntity(RestEntity entity) { if (entity instanceof Node) { runQuery(_MATCH_NODE_QUERY + " DELETE n", map("id", entity.getId())); + restAPI.removeFromCache(entity.getId()); } else if (entity instanceof Relationship) { - runQuery(_MATCH_REL_QUERY + " DELETE r", map("id", entity.getId())); + runQuery(_MATCH_REL_QUERY + " DELETE r", map("id", entity.getId())); } } @Override public void setPropertyOnEntity(RestEntity entity, String key, Object value) { if (entity instanceof Node) { - runQuery(_MATCH_NODE_QUERY + " SET n.`"+key+"` = {value} ", map("id", entity.getId(), "value", value)); + runQuery(_MATCH_NODE_QUERY + " SET n.`" + key + "` = {value} ", map("id", entity.getId(), "value", value)); } else if (entity instanceof Relationship) { - runQuery(_MATCH_REL_QUERY + " SET r.`"+key+"` = {value} ", map("id", entity.getId(), "value", value)); + runQuery(_MATCH_REL_QUERY + " SET r.`" + key + "` = {value} ", map("id", entity.getId(), "value", value)); } } @@ -384,22 +401,22 @@ public class RestAPICypherImpl implements RestAPI { @Override public void removeProperty(RestEntity entity, String key) { if (entity instanceof Node) { - runQuery(_MATCH_NODE_QUERY + " REMOVE n.`"+key+"`", map("id", entity.getId())); + runQuery(_MATCH_NODE_QUERY + " REMOVE n.`" + key + "`", map("id", entity.getId())); } else if (entity instanceof Relationship) { - runQuery(_MATCH_REL_QUERY + " REMOVE r.`"+key+"`", map("id", entity.getId())); + runQuery(_MATCH_REL_QUERY + " REMOVE r.`" + key + "`", map("id", entity.getId())); } } // todo handle within cypher tx @Override public RestNode getOrCreateNode(RestIndex index, String key, Object value, final Map properties, Collection labels) { - return restAPI.getOrCreateNode(index,key,value,properties,labels); + return restAPI.getOrCreateNode(index, key, value, properties, labels); } // todo handle within cypher tx @Override public RestRelationship getOrCreateRelationship(RestIndex index, String key, Object value, final RestNode start, final RestNode end, final String type, final Map properties) { - return restAPI.getOrCreateRelationship(index,key,value,start,end,type,properties); + return restAPI.getOrCreateRelationship(index, key, value, start, end, type, properties); } public CypherResult query(String statement, Map params) { @@ -407,20 +424,19 @@ public class RestAPICypherImpl implements RestAPI { } private CypherTransaction.Result runQuery(String statement, Map params) { - if (cypherTransaction.get() == null) { - return newCypherTransaction().commit(statement,params); + if (!txManager.isActive()) { + return newCypherTransaction().commit(statement, params); } - return cypherTransaction.get().send(statement,params); + return txManager.getCypherTransaction().send(statement, params); } - private CypherTransaction newCypherTransaction() { + public CypherTransaction newCypherTransaction() { return new CypherTransaction(this, CypherTransaction.ResultType.row); } public QueryResult> query(String statement, Map params, ResultConverter resultConverter) { - final CypherResult result = query(statement, params); - if (RestResultException.isExceptionResult(result.asMap())) throw new RestResultException(result.asMap()); - return RestQueryResult.toQueryResult(result, this, resultConverter); + CypherTransaction.Result result = runQuery(statement, params); + return new QueryResultBuilder<>(result, resultConverter); } @Override @@ -447,7 +463,7 @@ public class RestAPICypherImpl implements RestAPI { @Override @SuppressWarnings("unchecked") public RestIndex createIndex(Class type, String indexName, Map config) { - return restAPI.createIndex(type,indexName,config); + return restAPI.createIndex(type, indexName, config); } @Override @@ -472,7 +488,7 @@ public class RestAPICypherImpl implements RestAPI { @Override public void startAutoIndexingProperty(Class forClass, String s) { - restAPI.startAutoIndexingProperty(forClass,s); + restAPI.startAutoIndexingProperty(forClass, s); } @Override @@ -492,18 +508,18 @@ public class RestAPICypherImpl implements RestAPI { @Override public void removeFromIndex(RestIndex index, T entity, String key) { - restAPI.removeFromIndex(index,entity,key); + restAPI.removeFromIndex(index, entity, key); } @Override public void removeFromIndex(RestIndex index, T entity) { - restAPI.removeFromIndex(index,entity); + restAPI.removeFromIndex(index, entity); } @Override public void addToIndex(T entity, RestIndex index, String key, Object value) { - restAPI.addToIndex(entity,index,key,value); + restAPI.addToIndex(entity, index, key, value); } @Override @@ -516,6 +532,7 @@ public class RestAPICypherImpl implements RestAPI { public boolean hasToUpdate(long lastUpdate) { return restAPI.hasToUpdate(lastUpdate); } + @Override public IndexInfo indexInfo(final String indexType) { return restAPI.indexInfo(indexType); @@ -533,12 +550,12 @@ public class RestAPICypherImpl implements RestAPI { } @Override - public TraversalDescription createTraversalDescription() { + public RestTraversalDescription createTraversalDescription() { return restAPI.createTraversalDescription(); } public String getBaseUri() { - return restRequest.getUri(); + return restAPI.getBaseUri(); } @Override @@ -550,4 +567,16 @@ public class RestAPICypherImpl implements RestAPI { public RestEntity createRestEntity(Map data) { return restAPI.createRestEntity(data); } + + + public Iterable getAllNodes() { + String statement = "MATCH (n) " + _QUERY_RETURN_NODE; + Iterable> result = query(statement, null).getData(); + return new IterableWrapper>(result) { + @Override + protected Node underlyingObjectToObject(List row) { + return addToCache(toNode(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 4f8738379..ab0dd6093 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 @@ -44,6 +44,7 @@ import org.neo4j.rest.graphdb.query.RestQueryResult; import org.neo4j.rest.graphdb.transaction.NullTransaction; import org.neo4j.rest.graphdb.traversal.RestDirection; import org.neo4j.rest.graphdb.traversal.RestTraversal; +import org.neo4j.rest.graphdb.traversal.RestTraversalDescription; import org.neo4j.rest.graphdb.traversal.RestTraverser; import org.neo4j.rest.graphdb.util.JsonHelper; import org.neo4j.rest.graphdb.util.QueryResult; @@ -72,6 +73,8 @@ public class RestAPIImpl implements RestAPI { private long entityRefetchTimeInMillis = TimeUnit.SECONDS.toMillis(1000); //TODO move to cache private final RestEntityCache entityCache = new RestEntityCache(this); private RestEntityExtractor restEntityExtractor = new RestEntityExtractor(this); + private RestIndexManager restIndexManager = new RestIndexManager(this); + private Map indexInfos = new HashMap<>(); public RestAPIImpl(String uri) { this.restRequest = createRestRequest(uri, null, null); @@ -87,7 +90,7 @@ public class RestAPIImpl implements RestAPI { @Override public RestIndexManager index() { - return new RestIndexManager(this); + return restIndexManager; } @Override @@ -98,14 +101,20 @@ public class RestAPIImpl implements RestAPI { } if (force == Load.FromCache) return new RestNode(RestNode.nodeUri(this, id),this); - BatchRestAPI batchRestAPI = new BatchRestAPI(this); - RestNode node = batchRestAPI.getNodeById(id); -// RequestResult response = restRequest.get("node/" + id); -// if (response.statusIs(Status.NOT_FOUND)) { -// throw new NotFoundException("" + id); -// } -// Collection labels = getNodeLabels(id); -// RestNode node = new RestNode(id, labels, (Map) response.toMap(), this); +// BatchRestAPI batchRestAPI = new BatchRestAPI(this); +// RestNode node = batchRestAPI.getNodeById(id); + RequestResult response = restRequest.get("node/" + id); + if (response.statusIs(Status.NOT_FOUND)) { + throw new NotFoundException("" + id); + } + RestNode node; + Map data = (Map) response.toMap(); + if (response.isMap() && data.containsKey("metadata")) { + node = new RestNode(data, this); + } else { + Collection labels = getNodeLabels(id); + node = new RestNode(id, labels, data, this); + } return entityCache.addToCache(node); } @@ -119,6 +128,11 @@ public class RestAPIImpl implements RestAPI { return entityCache.getNode(id); } + @Override + public void removeFromCache(long id) { + entityCache.remove(id); + } + @Override public RestNode getNodeById(long id) { return getNodeById(id, Load.FromServer); @@ -442,8 +456,8 @@ public class RestAPIImpl implements RestAPI { @Override - public TraversalDescription createTraversalDescription() { - return new RestTraversal(); + public RestTraversalDescription createTraversalDescription() { + return RestTraversal.description(); } @Override @@ -521,8 +535,14 @@ public class RestAPIImpl implements RestAPI { } @Override public IndexInfo indexInfo(final String indexType) { + IndexInfo indexInfo = indexInfos.get(indexType); + if (indexInfo != null && !indexInfo.isExpired()) { + + } RequestResult response = restRequest.get("index/" + encode(indexType)); - return new RetrievedIndexInfo(response); + indexInfo = new RetrievedIndexInfo(response); + indexInfos.put(indexType,indexInfo); + return indexInfo; } @Override diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/batch/BatchRestAPI.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/batch/BatchRestAPI.java index 8191c44ce..d61abd3f9 100644 --- a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/batch/BatchRestAPI.java +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/batch/BatchRestAPI.java @@ -147,28 +147,4 @@ public class BatchRestAPI { } return uri + "/" + suffix; } - - - private static class BatchIndexInfo implements IndexInfo { - - @Override - public boolean checkConfig(String indexName, Map config) { - return true; - } - - @Override - public String[] indexNames() { - return new String[0]; - } - - @Override - public boolean exists(String indexName) { - return true; - } - - @Override - public Map getConfig(String name) { - return null; - } - } } diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/IndexInfo.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/IndexInfo.java index 9769d3453..a18219a89 100644 --- a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/IndexInfo.java +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/IndexInfo.java @@ -33,4 +33,6 @@ public interface IndexInfo { boolean exists(String indexName); Map getConfig(String name); + + boolean isExpired(); } diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RetrievedIndexInfo.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RetrievedIndexInfo.java index 92428af5c..44fb370d0 100644 --- a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RetrievedIndexInfo.java +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RetrievedIndexInfo.java @@ -22,6 +22,7 @@ package org.neo4j.rest.graphdb.index; import java.util.Collections; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; import org.neo4j.rest.graphdb.RequestResult; @@ -33,10 +34,14 @@ import com.sun.jersey.api.client.ClientResponse; */ public class RetrievedIndexInfo implements IndexInfo { private Map indexInfo; + private long expired; public RetrievedIndexInfo(RequestResult response) { if (response.statusIs(ClientResponse.Status.NO_CONTENT)) this.indexInfo = Collections.emptyMap(); - else this.indexInfo = (Map) response.toMap(); + else { + this.indexInfo = (Map) response.toMap(); + this.expired = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(10); + } } @Override @@ -72,4 +77,9 @@ public class RetrievedIndexInfo implements IndexInfo { public Map getConfig(String name) { return (Map) indexInfo.get(name); } + + @Override + public boolean isExpired() { + return System.currentTimeMillis() > expired; + } } 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 89d41d20a..920ac25e9 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 @@ -4,12 +4,13 @@ import com.sun.jersey.api.client.ClientResponse; import org.neo4j.helpers.collection.IterableWrapper; import org.neo4j.helpers.collection.IteratorWrapper; import org.neo4j.rest.graphdb.*; -import org.springframework.data.neo4j.mapping.RelationshipResult; import javax.ws.rs.core.Response; import java.util.*; +import static java.util.Arrays.asList; import static org.neo4j.helpers.collection.MapUtil.map; +import static org.neo4j.helpers.collection.MapUtil.stringMap; /** * @author mh @@ -144,14 +145,14 @@ public class CypherTransaction { add(statement,params); List results = send(transactionUrl()); if (results.size() > 0) return results.get(results.size() - 1); - else throw new RuntimeException("No Results after single send"); + throw new CypherTransactionExecutionException("Error Sending",asList(new Statement(statement,params,type)),errors("No.Results","No Results after single send")); } public Result commit(String statement, Map params) { add(statement,params); List results = commit(); if (results.size() > 0) return results.get(results.size() - 1); - else throw new RuntimeException("No Results after single commit"); + else throw new CypherTransactionExecutionException("Error Sending",asList(new Statement(statement,params,type)),errors("No.Results","No Results after single commit")); } public List send() { @@ -160,6 +161,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 return send(commitUrl()); } finally { commitUrl = null; @@ -170,16 +172,22 @@ public class CypherTransaction { try { RequestResult result = request.post(url, map("statements", statements)); if (result.statusIs(Response.Status.OK) || result.statusIs(Response.Status.CREATED)) { - return Result.toResults(handleResult(result), new ArrayList<>(statements), type); + ArrayList statementsCopy = new ArrayList<>(statements); + return Result.toResults(handleResult(result,statementsCopy), statementsCopy, type); } else { - throw new RuntimeException("Error executing statements: " + result.getStatus() + - " " + result.getText()); + List> errors = errors("Http." + result.getStatus(), result.getText()); + throw new CypherTransactionExecutionException("Error executing statements: " + result.getStatus() + + " " + result.getText(),statements, errors); } } finally { statements.clear(); } } + private List> errors(String code, String message) { + return asList(stringMap("code", code, "message", message)); + } + public void rollback() { if (transactionUrl != null) { request.delete(transactionUrl); @@ -197,10 +205,10 @@ public class CypherTransaction { } @SuppressWarnings("unchecked") - private List handleResult(RequestResult result) { + private List handleResult(RequestResult result, ArrayList statements) { Map resultData = result.toMap(); - Object errors = resultData.get("errors"); - if (errors != null && !((Collection)errors).isEmpty()) throw new RuntimeException("Error executing cypher statements "+errors); + 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(); commitUrl = (String) resultData.get("commit"); return (List) resultData.get("results"); diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/query/CypherTransactionExecutionException.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/query/CypherTransactionExecutionException.java new file mode 100644 index 000000000..527fc6fef --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/query/CypherTransactionExecutionException.java @@ -0,0 +1,36 @@ +package org.neo4j.rest.graphdb.query; + +import java.util.List; +import java.util.Map; + +/** + * @author mh + * @since 28.09.14 + */ +public class CypherTransactionExecutionException extends RuntimeException { + private final List statements; + private final List> errors; + + public CypherTransactionExecutionException(String msg, List statements, List> errors) { + super(msg + errors.toString()); + this.statements = statements; + this.errors = errors; + } + + public List getStatements() { + return statements; + } + + public List> getErrors() { + return errors; + } + + public boolean contains(String code, String message) { + for (Map error : errors) { + if (!code.equals(error.get("code"))) continue; + String msg = error.get("message"); + if (msg != null && msg.contains(message)) return true; + } + return false; + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/query/RestCypherTransactionManager.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/query/RestCypherTransactionManager.java new file mode 100644 index 000000000..eaa9b9890 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/query/RestCypherTransactionManager.java @@ -0,0 +1,115 @@ +package org.neo4j.rest.graphdb.query; + +import org.neo4j.graphdb.*; +import org.neo4j.rest.graphdb.RestAPICypherImpl; +import org.neo4j.rest.graphdb.transaction.RemoteCypherTransaction; + +import javax.transaction.*; +import javax.transaction.Transaction; +import javax.transaction.xa.XAResource; + +/** + * @author mh + * @since 28.09.14 + */ +public class RestCypherTransactionManager implements TransactionManager, Transaction { + private static final ThreadLocal cypherTransaction = new ThreadLocal<>(); + private final RestAPICypherImpl restAPICypher; + + public RestCypherTransactionManager(RestAPICypherImpl restAPICypher) { + this.restAPICypher = restAPICypher; + } + + + @Override + public void begin() throws NotSupportedException, SystemException { + beginTx(); + } + + public org.neo4j.graphdb.Transaction beginTx() { + RemoteCypherTransaction tx = cypherTransaction.get(); + if (tx != null && tx.isActive()) { + tx.beginInner(); + } else { + CypherTransaction newTx = restAPICypher.newCypherTransaction(); + tx = new RemoteCypherTransaction(newTx); + cypherTransaction.set(tx); + } + return tx; + } + + @Override + public void commit() throws HeuristicMixedException, HeuristicRollbackException, IllegalStateException, RollbackException, SecurityException, SystemException { + RemoteCypherTransaction tx = getRemoteCypherTransaction(); + tx.success(); + tx.close(); + } + + @Override + public boolean delistResource(XAResource xaRes, int flag) throws IllegalStateException, SystemException { + return false; + } + + @Override + public boolean enlistResource(XAResource xaRes) throws IllegalStateException, RollbackException, SystemException { + return false; + } + + @Override + public void registerSynchronization(Synchronization synch) throws IllegalStateException, RollbackException, SystemException { + + } + + @Override + public int getStatus() throws SystemException { + RemoteCypherTransaction tx = getRemoteCypherTransaction(); + if (tx == null || !tx.isActive()) return Status.STATUS_NO_TRANSACTION; + return tx.getStatus(); + } + + + @Override + public Transaction getTransaction() throws SystemException { + return this; + } + + @Override + public void rollback() throws IllegalStateException, SecurityException, SystemException { + RemoteCypherTransaction tx = getRemoteCypherTransaction(); + tx.failure(); + tx.close(); + } + + @Override + public void setRollbackOnly() throws IllegalStateException, SystemException { + getRemoteCypherTransaction().failure(); + } + + @Override + public void setTransactionTimeout(int seconds) throws SystemException { + // todo + } + + @Override + public Transaction suspend() throws SystemException { + return this; + } // todo we could implement suspend + + @Override + public void resume(Transaction tx) throws IllegalStateException, InvalidTransactionException, SystemException { + // todo we could implement suspend + } + + public boolean isActive() { + RemoteCypherTransaction tx = getRemoteCypherTransaction(); + return tx !=null && tx.isActive(); + } + + public CypherTransaction getCypherTransaction() { + return getRemoteCypherTransaction().getTransaction(); + } + + public RemoteCypherTransaction getRemoteCypherTransaction() { + return cypherTransaction.get(); + } +} 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 740559478..69d18375f 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 @@ -22,19 +22,31 @@ package org.neo4j.rest.graphdb.transaction; import org.neo4j.graphdb.*; import org.neo4j.rest.graphdb.query.CypherTransaction; +import javax.transaction.Status; + +import java.util.concurrent.atomic.AtomicInteger; + import static org.neo4j.helpers.collection.MapUtil.map; public class RemoteCypherTransaction implements Transaction { + int status = Status.STATUS_NO_TRANSACTION; boolean success, failure; - ThreadLocal tx; + CypherTransaction tx; + AtomicInteger innerCounter = new AtomicInteger(1); - public RemoteCypherTransaction(ThreadLocal tx) { + public RemoteCypherTransaction(CypherTransaction tx) { this.tx = tx; + status = Status.STATUS_ACTIVE; + } + + public void beginInner() { + innerCounter.incrementAndGet(); } public void success() { this.success = true; + if (!failure) status = Status.STATUS_COMMITTING; // ??? } public void finish() { @@ -43,24 +55,31 @@ public class RemoteCypherTransaction implements Transaction { @Override public void close() { + if (tx() != null && innerCounter.decrementAndGet() > 0) { + return; + } try { - if (success && !failure) + if (success && !failure) { tx().commit(); - else + status = Status.STATUS_COMMITTED; + } + else { tx().rollback(); + status = Status.STATUS_ROLLEDBACK; + } } finally { - tx.set(null); + tx = null; } } private CypherTransaction tx() { - CypherTransaction cypherTransaction = tx.get(); - if (cypherTransaction == null) throw new IllegalStateException("No transaction active"); - return cypherTransaction; + if (tx == null) throw new IllegalStateException("No transaction active"); + return tx; } public void failure() { this.failure = true; + status = Status.STATUS_MARKED_ROLLBACK; } @Override @@ -78,4 +97,17 @@ public class RemoteCypherTransaction implements Transaction { public Lock acquireReadLock(PropertyContainer propertyContainer) { return null; } + + public int getStatus() { + return status; + } + + + public CypherTransaction getTransaction() { + return tx; + } + + public boolean isActive() { + return tx != null; + } } diff --git a/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/SpringCypherRestGraphDatabase.java b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/SpringCypherRestGraphDatabase.java new file mode 100644 index 000000000..af2d5e7d0 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/SpringCypherRestGraphDatabase.java @@ -0,0 +1,171 @@ +/** + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.neo4j.rest; + +import org.neo4j.graphdb.*; +import org.neo4j.graphdb.index.Index; +import org.neo4j.rest.graphdb.CypherRestGraphDatabase; +import org.neo4j.rest.graphdb.RestAPI; +import org.neo4j.rest.graphdb.RestAPIImpl; +import org.neo4j.rest.graphdb.entity.RestNode; +import org.neo4j.rest.graphdb.index.RestIndex; +import org.neo4j.rest.graphdb.index.RestIndexManager; +import org.neo4j.rest.graphdb.transaction.NullTransaction; +import org.neo4j.rest.graphdb.transaction.NullTransactionManager; +import org.neo4j.rest.graphdb.util.Config; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.neo4j.conversion.DefaultConverter; +import org.springframework.data.neo4j.conversion.ResultConverter; +import org.springframework.data.neo4j.core.GraphDatabase; +import org.springframework.data.neo4j.support.index.NoSuchIndexException; +import org.springframework.data.neo4j.support.query.ConversionServiceQueryResultConverter; +import org.springframework.data.neo4j.support.query.CypherQueryEngine; +import org.springframework.data.neo4j.support.schema.SchemaIndexProvider; + +import javax.transaction.TransactionManager; +import java.util.Collection; +import java.util.Map; + +public class SpringCypherRestGraphDatabase extends CypherRestGraphDatabase implements GraphDatabase { + + private ConversionService conversionService; + private ResultConverter resultConverter; + private SchemaIndexProvider schemaIndexProvider; + + public SpringCypherRestGraphDatabase(RestAPI api){ + super(api); + schemaIndexProvider = new SchemaIndexProvider(this); + } + + public SpringCypherRestGraphDatabase(String uri) { + this( new RestAPIImpl( uri ) ); + } + + public SpringCypherRestGraphDatabase(String uri, String user, String password) { + this(new RestAPIImpl( uri, user, password )); + } + + @Override + public Node createNode(Map props, Collection labels) { + RestAPI restAPI = super.getRestAPI(); + return restAPI.createNode(props,labels); + } + + @Override + public Node getOrCreateNode(String indexName, String key, Object value, final Map properties, Collection labels) { + if (indexName ==null || key == null || value==null) throw new IllegalArgumentException("Unique index "+ indexName +" key "+key+" value must not be null"); + final RestIndex nodeIndex = index().forNodes(indexName); + return getRestAPI().getOrCreateNode(nodeIndex, key, value, properties, labels); + } + + @Override + public Node merge(String labelName, String key, Object value, final Map nodeProperties, Collection labels) { + return getRestAPI().merge(labelName,key,value,nodeProperties, labels); + } + + + @Override + public Relationship getOrCreateRelationship(String indexName, String key, Object value, Node startNode, Node endNode, String type, Map properties) { + @SuppressWarnings("unchecked") final RestIndex relIndex = (RestIndex) index().forRelationships(indexName); + return getRestAPI().getOrCreateRelationship(relIndex,key,value,(RestNode) startNode,(RestNode) endNode,type, properties); + } + + @Override + public Relationship createRelationship(Node startNode, Node endNode, RelationshipType type, Map properties) { + return super.getRestAPI().createRelationship(startNode, endNode, type, properties); + } + + @Override + public Index getIndex(String indexName) { + try { + return super.getRestAPI().getIndex(indexName); + } catch (IllegalArgumentException iea) { + throw new NoSuchIndexException(indexName); + } + } + + @Override + public Index createIndex(Class type, String indexName, org.springframework.data.neo4j.support.index.IndexType indexType) { + return super.getRestAPI().createIndex(type, indexName, indexType.getConfig()); + } + + @SuppressWarnings("unchecked") + @Override + public CypherQueryEngine queryEngine(final ResultConverter resultConverter) { + return new SpringRestCypherQueryEngine(getRestAPI(),resultConverter); + } + + @Override + public CypherQueryEngine queryEngine() { + return queryEngine(createResultConverter()); + } + + @Override + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + private ResultConverter createResultConverter() { + if (resultConverter!=null) return resultConverter; + if (conversionService != null) { + this.resultConverter = new ConversionServiceQueryResultConverter(conversionService); + } else { + this.resultConverter = new DefaultConverter(); + } + return resultConverter; + } + + @Override + public boolean transactionIsRunning() { + return super.getTxManager().isActive(); + } + + @Override + public TransactionManager getTransactionManager() { + return super.getTxManager(); + } + + @Override + public void remove(Node node) { + removeFromIndexes(node); // todo should we do this by default? + node.delete(); + } + + @Override + public void remove(Relationship relationship) { + removeFromIndexes(relationship); // todo should we do this by default, even if it might not be indexed? + relationship.delete(); + } + + @Override + public void setResultConverter(ResultConverter resultConverter) { + this.resultConverter = resultConverter; + } + + private void removeFromIndexes(Node node) { + final RestIndexManager indexManager = index(); + for (String indexName : indexManager.nodeIndexNames()) { + indexManager.forNodes(indexName).remove(node); + } + } + + private void removeFromIndexes(Relationship relationship) { + final RestIndexManager indexManager = index(); + for (String indexName : indexManager.relationshipIndexNames()) { + indexManager.forRelationships(indexName).remove(relationship); + } + } +} diff --git a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestEntityTests.java b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestEntityTests.java index fec7ef9e7..41e0b15d3 100644 --- a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestEntityTests.java +++ b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestEntityTests.java @@ -32,7 +32,7 @@ public class RestEntityTests extends RestTestBase { @Test public void testSetProperty() { - Node node = restGraphDatabase.createNode(); + Node node = createNode(); node.setProperty("name", "test"); Node node2 = restGraphDatabase.getNodeById(node.getId()); assertEquals("test", node2.getProperty("name")); @@ -40,7 +40,7 @@ public class RestEntityTests extends RestTestBase { @Test public void testSetStringArrayProperty() { - Node node = restGraphDatabase.createNode(); + Node node = createNode(); node.setProperty("name", new String[]{"test"}); Node node2 = restGraphDatabase.getNodeById(node.getId()); Assert.assertArrayEquals( new String[]{"test"}, (String[])node2.getProperty( "name" ) ); @@ -48,7 +48,7 @@ public class RestEntityTests extends RestTestBase { @Test public void testSetDoubleArrayProperty() { double[] data = {0, 1, 2}; - Node node = restGraphDatabase.createNode(); + Node node = createNode(); node.setProperty("data", data); Node node2 = restGraphDatabase.getNodeById(node.getId()); Assert.assertTrue("same double array",Arrays.equals( data, (double[])node2.getProperty( "data" ) )); @@ -56,7 +56,7 @@ public class RestEntityTests extends RestTestBase { @Test public void testRemoveProperty() { - Node node = restGraphDatabase.createNode(); + Node node = createNode(); node.setProperty( "name", "test" ); assertEquals("test", node.getProperty("name")); node.removeProperty( "name" ); @@ -65,7 +65,7 @@ public class RestEntityTests extends RestTestBase { @Test(expected = NotFoundException.class) public void testRemoveNode() { - Node node = restGraphDatabase.createNode(); + Node node = createNode(); node.setProperty( "name", "test" ); final long nodeId = node.getId(); assertEquals("test", node.getProperty("name")); @@ -75,8 +75,8 @@ public class RestEntityTests extends RestTestBase { @Test(expected = NotFoundException.class) public void testRemoveRelationship() { - Node refNode = restGraphDatabase.createNode(); - Node node = restGraphDatabase.createNode(); + Node refNode = createNode(); + Node node = createNode(); Relationship rel = restGraphDatabase.createRelationship(refNode, node, Type.TEST, map("name","test")); final long relId = rel.getId(); assertEquals("test", rel.getProperty("name")); @@ -87,8 +87,8 @@ public class RestEntityTests extends RestTestBase { @Test public void testSetPropertyOnRelationship() { - Node refNode = restGraphDatabase.createNode(); - Node node = restGraphDatabase.createNode(); + Node refNode = createNode(); + Node node = createNode(); Relationship rel = refNode.createRelationshipTo( node, Type.TEST ); rel.setProperty( "name", "test" ); assertEquals("test", rel.getProperty("name")); @@ -98,8 +98,8 @@ public class RestEntityTests extends RestTestBase { @Test public void testRemovePropertyOnRelationship() { - Node refNode = restGraphDatabase.createNode(); - Node node = restGraphDatabase.createNode(); + Node refNode = createNode(); + Node node = createNode(); Relationship rel = refNode.createRelationshipTo( node, Type.TEST ); rel.setProperty( "name", "test" ); assertEquals("test", rel.getProperty("name")); diff --git a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestGraphDbTests.java b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestGraphDbTests.java index 633c078b0..e23b4ca45 100644 --- a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestGraphDbTests.java +++ b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestGraphDbTests.java @@ -26,21 +26,21 @@ public class RestGraphDbTests extends RestTestBase { @Test public void testGetRefNode() { - Node refNode = restGraphDatabase.createNode(); + Node refNode = createNode(); Node nodeById = restGraphDatabase.getNodeById( refNode.getId() ); Assert.assertEquals( refNode, nodeById ); } @Test public void testCreateNode() { - Node node = restGraphDatabase.createNode(); + Node node = createNode(); Assert.assertEquals( node, restGraphDatabase.getNodeById( node.getId() ) ); } @Test public void testCreateRelationship() { - Node refNode = restGraphDatabase.createNode(); - Node node = restGraphDatabase.createNode(); + Node refNode = createNode(); + Node node = createNode(); Relationship rel = refNode.createRelationshipTo( node, Type.TEST ); Relationship foundRelationship = IsRelationshipToNodeMatcher.relationshipFromTo( refNode.getRelationships( Type.TEST, Direction.OUTGOING ), refNode, node ); Assert.assertNotNull( "found relationship", foundRelationship ); @@ -53,8 +53,8 @@ public class RestGraphDbTests extends RestTestBase { @Test public void testBasic() { - Node refNode = restGraphDatabase.createNode(); - Node node = restGraphDatabase.createNode(); + Node refNode = createNode(); + Node node = createNode(); Relationship rel = refNode.createRelationshipTo( node, DynamicRelationshipType.withName( "TEST" ) ); rel.setProperty( "date", new Date().getTime() ); diff --git a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestIndexTests.java b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestIndexTests.java index 05b62bce8..f6dd9b06a 100644 --- a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestIndexTests.java +++ b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestIndexTests.java @@ -111,23 +111,23 @@ public class RestIndexTests extends RestTestBase { } private Index nodeIndex() { - return restGraphDatabase.index().forNodes(NODE_INDEX_NAME); + return index().forNodes(NODE_INDEX_NAME); } private RelationshipIndex relationshipIndex() { - return restGraphDatabase.index().forRelationships(REL_INDEX_NAME); + return index().forRelationships(REL_INDEX_NAME); } @Test public void testNodeIndexIsListed() { nodeIndex().add(node(), "name", "test"); - Assert.assertTrue("node index name listed", Arrays.asList(restGraphDatabase.index().nodeIndexNames()).contains(NODE_INDEX_NAME)); + Assert.assertTrue("node index name listed", Arrays.asList(index().nodeIndexNames()).contains(NODE_INDEX_NAME)); } @Test public void testRelationshipIndexIsListed() { relationshipIndex().add(relationship(), "name", "test"); - Assert.assertTrue("relationship index name listed", Arrays.asList(restGraphDatabase.index().relationshipIndexNames()).contains(REL_INDEX_NAME)); + Assert.assertTrue("relationship index name listed", Arrays.asList(index().relationshipIndexNames()).contains(REL_INDEX_NAME)); } } 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 20ffe4770..cff5233e8 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 @@ -20,10 +20,12 @@ package org.springframework.data.neo4j.rest.support; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.runners.Parameterized; import org.neo4j.graphdb.Direction; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.index.IndexManager; import org.neo4j.rest.graphdb.ExecutingRestRequest; import org.neo4j.rest.graphdb.RequestResult; import org.neo4j.server.NeoServer; @@ -31,6 +33,8 @@ import org.neo4j.server.WrappingNeoServerBootstrapper; 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; import java.util.Iterator; @@ -40,7 +44,7 @@ import static org.junit.Assert.assertEquals; public class RestTestBase { protected static ImpermanentGraphDatabase db; - protected SpringRestGraphDatabase restGraphDatabase; + protected GraphDatabase restGraphDatabase; private static final String HOSTNAME = "127.0.0.1"; public static final int PORT = 7470; protected static NeoServer neoServer = null; @@ -78,8 +82,13 @@ public class RestTestBase { @Before public void setUp() throws Exception { cleanDb(); - restGraphDatabase = new SpringRestGraphDatabase(SERVER_ROOT_URI); - refNode = restGraphDatabase.createNode(); +// restGraphDatabase = new SpringRestGraphDatabase(SERVER_ROOT_URI); + restGraphDatabase = new SpringCypherRestGraphDatabase(SERVER_ROOT_URI); + refNode = createNode(); + } + + public Node createNode() { + return restGraphDatabase.createNode(null,null); } public static void cleanDb() { @@ -100,7 +109,11 @@ public class RestTestBase { protected Relationship relationship() { Iterator it = node().getRelationships(Direction.OUTGOING).iterator(); if (it.hasNext()) return it.next(); - return node().createRelationshipTo(restGraphDatabase.createNode(), Type.TEST); + return node().createRelationshipTo(createNode(), Type.TEST); + } + + protected IndexManager index() { + return ((GraphDatabaseService)restGraphDatabase).index(); } protected Node node() { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/JtaTransactionManagerFactoryBean.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/JtaTransactionManagerFactoryBean.java index 7e88bddc2..f2a239305 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/JtaTransactionManagerFactoryBean.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/JtaTransactionManagerFactoryBean.java @@ -23,20 +23,23 @@ import org.neo4j.kernel.GraphDatabaseAPI; import org.neo4j.kernel.impl.transaction.SpringTransactionManager; import org.neo4j.kernel.impl.transaction.UserTransactionImpl; import org.springframework.beans.factory.FactoryBean; +import org.springframework.data.neo4j.core.GraphDatabase; import org.springframework.transaction.jta.JtaTransactionManager; import org.springframework.transaction.jta.UserTransactionAdapter; +import java.lang.reflect.InvocationTargetException; + public class JtaTransactionManagerFactoryBean implements FactoryBean { private final JtaTransactionManager jtaTransactionManager; - public JtaTransactionManagerFactoryBean( GraphDatabaseService gds ) throws Exception + public JtaTransactionManagerFactoryBean( GraphDatabaseService gds ) { jtaTransactionManager = create( gds ); } @Override - public JtaTransactionManager getObject() throws Exception + public JtaTransactionManager getObject() { return jtaTransactionManager; } @@ -53,21 +56,31 @@ public class JtaTransactionManagerFactoryBean implements FactoryBean T createDynamically( Class requiredClass, Class argumentClass, Object gds ) throws Exception + private T createDynamically( Class requiredClass, Class argumentClass, Object gds ) { - return requiredClass.getDeclaredConstructor( argumentClass ).newInstance( gds ); + try { + return requiredClass.getDeclaredConstructor( argumentClass ).newInstance( gds ); + } catch (InstantiationException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException("Error accessing constructor of class "+requiredClass+ " for parameter type "+argumentClass,e); + } } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/Neo4jConfiguration.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/Neo4jConfiguration.java index 01be1b9b4..092541189 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/Neo4jConfiguration.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/config/Neo4jConfiguration.java @@ -54,6 +54,8 @@ import org.springframework.data.neo4j.support.typerepresentation.TypeRepresentat import org.springframework.data.neo4j.support.typesafety.TypeSafetyPolicy; import org.springframework.data.support.IsNewStrategyFactory; import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.TransactionManagementConfigurer; + import javax.validation.Validator; import java.util.Arrays; @@ -69,7 +71,7 @@ import javax.enterprise.inject.Produces; * @author Thomas Risberg */ @Configuration -public abstract class Neo4jConfiguration { +public abstract class Neo4jConfiguration { // implements TransactionManagementConfigurer { private GraphDatabaseService graphDatabaseService; private ConversionService conversionService; @@ -112,7 +114,7 @@ public abstract class Neo4jConfiguration { factoryBean.setRelationshipTypeRepresentationStrategy(relationshipTypeRepresentationStrategy()); factoryBean.setRelationshipEntityInstantiator(graphRelationshipInstantiator()); - factoryBean.setTransactionManager(neo4jTransactionManager()); + factoryBean.setTransactionManager(neo4jTransactionManager(getGraphDatabaseService())); factoryBean.setGraphDatabase(graphDatabase()); factoryBean.setIsNewStrategyFactory(isNewStrategyFactory()); factoryBean.setTypeSafetyPolicy(typeSafetyPolicy()); @@ -237,10 +239,17 @@ public abstract class Neo4jConfiguration { @Bean(name = {"neo4jTransactionManager","transactionManager"}) @Qualifier("neo4jTransactionManager") - public PlatformTransactionManager neo4jTransactionManager() throws Exception { - return new JtaTransactionManagerFactoryBean(getGraphDatabaseService()).getObject(); + @DependsOn("graphDatabaseService") + public PlatformTransactionManager neo4jTransactionManager(GraphDatabaseService graphDatabaseService) { + JtaTransactionManagerFactoryBean jtaTransactionManagerFactoryBean = new JtaTransactionManagerFactoryBean(graphDatabaseService); + return jtaTransactionManagerFactoryBean.getObject(); } +// @Override +// public PlatformTransactionManager annotationDrivenTransactionManager() { +// return neo4jTransactionManager(); +// } + @Bean public EntityIndexCreator entityIndexCreator() throws Exception { return new EntityIndexCreator(