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
This commit is contained in:
2
pom.xml
2
pom.xml
@@ -38,7 +38,7 @@
|
||||
<source.level>1.7</source.level>
|
||||
<target.level>1.7</target.level>
|
||||
|
||||
<neo4j.version>2.1.4</neo4j.version>
|
||||
<neo4j.version>2.1.5</neo4j.version>
|
||||
|
||||
<neo4j.spatial.version>0.13-neo4j-2.1.4</neo4j.spatial.version>
|
||||
<neo4j-cypher-dsl.version>2.0.1</neo4j-cypher-dsl.version>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<neo4j:repositories base-package="org.neo4j.cineasts.repository"/>
|
||||
|
||||
<!--<bean id="graphDatabaseService" class="org.neo4j.test.ImpermanentGraphDatabase"/>-->
|
||||
<bean id="graphDatabaseService" class="org.springframework.data.neo4j.rest.SpringRestGraphDatabase" scope="singleton">
|
||||
<bean id="graphDatabaseService" class="org.springframework.data.neo4j.rest.SpringCypherRestGraphDatabase" scope="singleton">
|
||||
<constructor-arg index="0" value="http://localhost:7470/db/data" />
|
||||
</bean>
|
||||
<bean class="org.neo4j.cineasts.movieimport.MovieDbApiClient">
|
||||
@@ -26,4 +26,4 @@
|
||||
<constructor-arg value="target/json-data"/>
|
||||
</bean>
|
||||
<tx:annotation-driven mode="aspectj"/>
|
||||
</beans>
|
||||
</beans>
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
<properties>
|
||||
<validation>1.0.0.GA</validation>
|
||||
<jersey.version>1.9</jersey.version>
|
||||
<neo4j.version>2.1.4</neo4j.version>
|
||||
<neo4j-rest-graphdb.version>2.0.1</neo4j-rest-graphdb.version>
|
||||
<neo4j.version>2.1.5</neo4j.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<String> 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<List<Object>> result = query(GET_NODE_QUERY, map("id", id)).getData().iterator();
|
||||
Iterator<List<Object>> 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<List<Object>> result = query(GET_REL_QUERY, map("id", id)).getData().iterator();
|
||||
Iterator<List<Object>> 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<String, Object> props, Collection<String> labels) {
|
||||
Iterator<List<Object>> result = query(createNodeQuery(labels), map("props", props(props))).getData().iterator();
|
||||
Iterator<List<Object>> 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<String, Object> params = map("props", props, "value", value);
|
||||
Iterator<List<Object>> result = query(mergeQuery(labelName, key, labels), params).getData().iterator();
|
||||
Iterator<List<Object>> 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<String> 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 <S extends PropertyContainer> IndexHits<S> getIndex(Class<S> 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 <S extends PropertyContainer> IndexHits<S> queryIndex(Class<S> 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<String, Object> params) {
|
||||
return new CypherTxResult(runQuery(statement, params));
|
||||
return new CypherTxResult(runQuery(statement, params,true));
|
||||
}
|
||||
|
||||
private List<CypherTransaction.Result> runQueries(Collection<Statement> statements) {
|
||||
@@ -438,11 +443,14 @@ public class RestAPICypherImpl implements RestAPI {
|
||||
}
|
||||
}
|
||||
|
||||
private CypherTransaction.Result runQuery(String statement, Map<String, Object> params) {
|
||||
private CypherTransaction.Result runQuery(String statement, Map<String, Object> 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<String, Object> params) {
|
||||
return runQuery(statement,params,false);
|
||||
}
|
||||
|
||||
public CypherTransaction newCypherTransaction() {
|
||||
@@ -450,8 +458,53 @@ public class RestAPICypherImpl implements RestAPI {
|
||||
}
|
||||
|
||||
public QueryResult<Map<String, Object>> query(String statement, Map<String, Object> 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<String, Object>,Map<String, Object>>(result) {
|
||||
@Override
|
||||
protected Map<String, Object> underlyingObjectToObject(Map<String, Object> value) {
|
||||
return convertRestEntitiesInRow(value);
|
||||
}
|
||||
};
|
||||
return new QueryResultBuilder<>(it, resultConverter); // new RestEntityConverter(resultConverter));
|
||||
}
|
||||
|
||||
private Map<String, Object> convertRestEntitiesInRow(Map<String, Object> value) {
|
||||
Map<String,Object> map= value;
|
||||
for (Map.Entry<String, Object> 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<String,Object> map= (Map<String, Object>) value;
|
||||
for (Map.Entry<String, Object> 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 <T extends PropertyContainer> RestIndex<T> getIndex(String indexName) {
|
||||
return restAPI.getIndex(indexName);
|
||||
final RestIndexManager index = this.index();
|
||||
if (index.existsForNodes(indexName)) return (RestIndex<T>) index.forNodes(indexName);
|
||||
if (index.existsForRelationships(indexName)) return (RestIndex<T>) 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 <T extends PropertyContainer> RestIndex<T> createIndex(Class<T> type, String indexName, Map<String, String> config) {
|
||||
return restAPI.createIndex(type, indexName, config);
|
||||
if (Node.class.isAssignableFrom(type)) {
|
||||
return (RestIndex<T>) index().forNodes( indexName, config);
|
||||
}
|
||||
if (Relationship.class.isAssignableFrom(type)) {
|
||||
return (RestIndex<T>) 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<String, Object> params = map("id_n", start.getId(), "ids_m", nodeIds(endNodes));
|
||||
List<CypherTransaction.Result> 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<List<Object>> mergeResults = results.get(1).getRows();
|
||||
return new IterableWrapper<Relationship,List<Object>>(mergeResults) {
|
||||
@Override
|
||||
@@ -641,7 +703,7 @@ public class RestAPICypherImpl implements RestAPI {
|
||||
|
||||
public Iterable<Node> getAllNodes() {
|
||||
String statement = "MATCH (n) " + _QUERY_RETURN_NODE;
|
||||
Iterable<List<Object>> result = query(statement, null).getData();
|
||||
Iterable<List<Object>> result = runQuery(statement, null).getRows();
|
||||
return new IterableWrapper<Node, List<Object>>(result) {
|
||||
@Override
|
||||
protected Node underlyingObjectToObject(List<Object> row) {
|
||||
|
||||
@@ -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<String> labels = (List<String>) 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<Map<String, Object>> batchRequestData) {
|
||||
return restRequest.post("batch",batchRequestData);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.neo4j.rest.graphdb;
|
||||
|
||||
/**
|
||||
* @author mh
|
||||
* @since 11.11.14
|
||||
*/
|
||||
public interface RestAPIProvider {
|
||||
RestAPI getRestAPI();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Map> getNodes(Map data) {
|
||||
Map graph = (Map) data.get(name());
|
||||
return (List)graph.get("nodes");
|
||||
}
|
||||
public List<Map> getRelationships(Map data) {
|
||||
Map graph = (Map) data.get(name());
|
||||
return (List)graph.get("relationships");
|
||||
}
|
||||
}, row, rest;
|
||||
|
||||
public List<Object> get(Map data) {
|
||||
@@ -69,14 +76,34 @@ public class CypherTransaction {
|
||||
private static Result toResult(Map resultData, Statement statement, final ResultType type) {
|
||||
List<String> columns = (List<String>) resultData.get("columns");
|
||||
List<Map> rowsData = (List<Map>) resultData.get("data");
|
||||
final boolean replace = statement.doReplace();
|
||||
Iterable<List<Object>> rows = new IterableWrapper<List<Object>,Map>(rowsData) {
|
||||
protected List<Object> underlyingObjectToObject(Map map) {
|
||||
return type.get(map);
|
||||
List<Object> row = type.get(map);
|
||||
List graph = ResultType.graph.get(map);
|
||||
if (replace) replaceGraphElements(row, (List<Map>) graph);
|
||||
return row;
|
||||
}
|
||||
};
|
||||
return new Result(columns, rows, statement);
|
||||
}
|
||||
|
||||
// todo hack !!
|
||||
private static void replaceGraphElements(List<Object> row, List<Map> 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<String> getColumns() {
|
||||
return columns;
|
||||
}
|
||||
@@ -111,10 +138,12 @@ public class CypherTransaction {
|
||||
private final String statement;
|
||||
private final ResultType type;
|
||||
private final Map<String, Object> parameters;
|
||||
private final boolean replace;
|
||||
|
||||
public Statement(String query, Map<String, Object> parameters, ResultType type) {
|
||||
public Statement(String query, Map<String, Object> parameters, ResultType type, boolean replace) {
|
||||
this.statement = query;
|
||||
this.type = type;
|
||||
this.replace = replace;
|
||||
this.parameters = parameters == null ? Collections.<String,Object>emptyMap() : parameters;
|
||||
}
|
||||
|
||||
@@ -127,8 +156,14 @@ public class CypherTransaction {
|
||||
}
|
||||
|
||||
public List<String> 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<String,Object> params) {
|
||||
statements.add(new Statement(statement,params,type));
|
||||
public void add(String statement, Map<String, Object> params) {
|
||||
add(statement,params,false);
|
||||
}
|
||||
public void add(String statement, Map<String, Object> params, boolean replace) {
|
||||
statements.add(new Statement(statement,params,type, replace));
|
||||
}
|
||||
|
||||
public Result send(String statement, Map<String, Object> params) {
|
||||
return send(statement,params,false);
|
||||
}
|
||||
|
||||
public Result send(String statement, Map<String,Object> params) {
|
||||
add(statement,params);
|
||||
public Result send(String statement, Map<String, Object> params, boolean replace) {
|
||||
add(statement,params, replace);
|
||||
List<Result> 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<String,Object> params) {
|
||||
add(statement,params);
|
||||
public Result commit(String statement, Map<String, Object> params) {
|
||||
return commit(statement,params,false);
|
||||
}
|
||||
|
||||
public Result commit(String statement, Map<String, Object> params, boolean replace) {
|
||||
add(statement,params, replace);
|
||||
List<Result> 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<Result> send() {
|
||||
@@ -170,7 +215,7 @@ public class CypherTransaction {
|
||||
|
||||
public List<Result> 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<Map> handleResult(RequestResult result, ArrayList<Statement> statements) {
|
||||
Map<?, ?> resultData = result.toMap();
|
||||
List<Map<String,String>> errors = (List<Map<String, String>>) 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<Map>) resultData.get("results");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<T> implements QueryResult<T> {
|
||||
private CypherTransaction.Result cypherResult;
|
||||
private Iterable<T> result;
|
||||
private final ResultConverter defaultConverter;
|
||||
private final boolean isClosableIterable;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -50,7 +50,7 @@ public class RestAPITest extends RestTestBase {
|
||||
|
||||
@Before
|
||||
public void init(){
|
||||
this.restAPI = ((RestGraphDatabase)getRestGraphDb()).getRestAPI();
|
||||
this.restAPI = ((RestAPIProvider)getRestGraphDb()).getRestAPI();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ public class RestTestBase {
|
||||
}
|
||||
|
||||
protected GraphDatabaseService createRestGraphDatabase() {
|
||||
return new RestGraphDatabase(SERVER_ROOT_URI);
|
||||
return new CypherRestGraphDatabase(SERVER_ROOT_URI);
|
||||
}
|
||||
|
||||
@After
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Map<String,Object>> result = cypher.query("CREATE (person1 { personId: {id}, started: {started} }) return person1",
|
||||
map("id", 1, "started", System.currentTimeMillis()));
|
||||
|
||||
@@ -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<CypherTransaction.Result> 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<CypherTransaction.Result> commit = transaction.commit();
|
||||
assertEquals(1, commit.size());
|
||||
Node node = getRestGraphDb().getNodeById(((Number) result.getRows().iterator().next().get(0)).longValue());
|
||||
assertEquals("John",node.getProperty("name"));
|
||||
}
|
||||
|
||||
@@ -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<List<Object>> rows = result.getRows().iterator();
|
||||
assertEquals(true,rows.hasNext());
|
||||
assertEquals(Arrays.<Object>asList(42), rows.next());
|
||||
assertEquals(false,rows.hasNext());
|
||||
assertEquals("RETURN 42", result.getStatement().getStatement());
|
||||
assertEquals(Collections.<String,Object>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<CypherTransaction.Result> 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<CypherTransaction.Result> 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<CypherTransaction.Result> 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);
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<Person> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
<bean id="graphDatabaseService" class="org.springframework.data.neo4j.rest.SpringRestGraphDatabase" scope="singleton">
|
||||
<bean id="graphDatabaseService" class="org.springframework.data.neo4j.rest.SpringCypherRestGraphDatabase" destroy-method="shutdown">
|
||||
<constructor-arg index="0" value="http://localhost:7470/db/data" />
|
||||
</bean>
|
||||
<alias name="graphDatabaseService" alias="graphDatabase"/>
|
||||
|
||||
@@ -43,7 +43,7 @@ public class DefaultConverter<T,R> implements ResultConverter<T,R> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -144,7 +144,7 @@ public class Main {
|
||||
<programlisting language="xml"><![CDATA[
|
||||
<neo4j:config graphDatabaseService="graphDatabaseService"/>
|
||||
<bean id="graphDatabaseService"
|
||||
class="org.springframework.data.neo4j.rest.SpringRestGraphDatabase">
|
||||
class="org.springframework.data.neo4j.rest.SpringCypherRestGraphDatabase">
|
||||
<constructor-arg index="0" value="${NEO4J_REST_URL}" />
|
||||
<constructor-arg index="1" value="${NEO4J_LOGIN}" />
|
||||
<constructor-arg index="2" value="${NEO4J_PASSWORD}" />
|
||||
|
||||
@@ -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 `<neo4j:config>` 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 `<neo4j:config>`, 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]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-neo4j-rest</artifactId>
|
||||
<version>2.1.0.RELEASE</version>
|
||||
<version>3.3.0.M1</version>
|
||||
</dependency>
|
||||
----
|
||||
====
|
||||
|
||||
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]
|
||||
----
|
||||
<neo4j:config graphDatabaseService="graphDatabaseService"/>
|
||||
|
||||
<bean id="graphDatabaseService" class="org.springframework.data.neo4j.rest.SpringRestGraphDatabase">
|
||||
<bean id="graphDatabaseService" class="org.springframework.data.neo4j.rest.SpringCypherRestGraphDatabase">
|
||||
<constructor-arg value="http://localhost:7474/db/data/" index="0"/>
|
||||
<!-- for running against a server requiring authentication
|
||||
<constructor-arg value="username" index="1"/>
|
||||
@@ -96,7 +122,10 @@ Now, you set up the normal Spring Data Neo4j configuration, but point the databa
|
||||
----
|
||||
====
|
||||
|
||||
Your project is now set up to work against a remote Neo4j Server.
|
||||
Your project is now set up to work with a remote Neo4j Server.
|
||||
|
||||
For traversals and Cypher graph queries it is sensible to forward those to the remote endpoint and execute them there instead of walking the graph over the wire. SpringRestGraphDatabase already supports that by providing methods that forward to the remote instance. (e.g. `queryEngineFor(), index() and createTraversalDescription()`). Please use those methods when interacting with a remote server for optimal performance. Those methods are also used by the Neo4jTemplate and the mapping infrastructure automatically.
|
||||
For direct execution of Cypher graph queries and graph traversals it is sensible to forward those to the remote side and execute them on the server.
|
||||
`SpringCypherRestGraphDatabase` already supports this approach by providing appropriate methods. (e.g. `query()`, `queryEngineFor(), index()` and `createTraversalDescription()`).
|
||||
Please use those methods when interacting with a remote server for better performance.
|
||||
Those methods are also used by the Neo4jTemplate and the mapping infrastructure implementation.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user