diff --git a/spring-data-neo4j-rest/pom.xml b/spring-data-neo4j-rest/pom.xml
index 38951b14f..8dda616ee 100644
--- a/spring-data-neo4j-rest/pom.xml
+++ b/spring-data-neo4j-rest/pom.xml
@@ -65,7 +65,6 @@
org.neo4j
neo4j-lucene-index
${neo4j.version}
- test
@@ -117,26 +116,6 @@
test
-
- org.neo4j
- neo4j-rest-graphdb
- ${neo4j-rest-graphdb.version}
-
-
- org.neo4j
- neo4j-kernel
-
-
- org.neo4j
- neo4j-lucene-index
-
-
- org.neo4j
- server-api
-
-
-
-
org.neo4j.app
neo4j-server
diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/AbstractRemoteDatabase.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/AbstractRemoteDatabase.java
new file mode 100644
index 000000000..9db4c9bc8
--- /dev/null
+++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/AbstractRemoteDatabase.java
@@ -0,0 +1,73 @@
+/**
+ * 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.event.KernelEventHandler;
+import org.neo4j.graphdb.event.TransactionEventHandler;
+import org.neo4j.kernel.GraphDatabaseAPI;
+import org.neo4j.kernel.IdGeneratorFactory;
+import org.neo4j.kernel.KernelData;
+import org.neo4j.kernel.TransactionBuilder;
+import org.neo4j.kernel.guard.Guard;
+import org.neo4j.kernel.impl.core.KernelPanicEventGenerator;
+import org.neo4j.kernel.impl.core.NodeManager;
+import org.neo4j.kernel.impl.nioneo.store.StoreId;
+import org.neo4j.kernel.impl.persistence.PersistenceSource;
+import org.neo4j.kernel.impl.transaction.LockManager;
+import org.neo4j.kernel.impl.transaction.XaDataSourceManager;
+import org.neo4j.kernel.impl.transaction.xaframework.TxIdGenerator;
+import org.neo4j.kernel.impl.util.StringLogger;
+import org.neo4j.kernel.info.DiagnosticsManager;
+import org.neo4j.rest.graphdb.transaction.NullTransaction;
+
+import javax.transaction.TransactionManager;
+import java.util.Collection;
+
+abstract class AbstractRemoteDatabase implements GraphDatabaseAPI {
+ public Transaction beginTx() {
+ return new NullTransaction();
+ }
+
+ public TransactionEventHandler registerTransactionEventHandler( TransactionEventHandler tTransactionEventHandler ) {
+ throw new UnsupportedOperationException();
+ }
+
+ public TransactionEventHandler unregisterTransactionEventHandler( TransactionEventHandler tTransactionEventHandler ) {
+ throw new UnsupportedOperationException();
+ }
+
+ public KernelEventHandler registerKernelEventHandler( KernelEventHandler kernelEventHandler ) {
+ throw new UnsupportedOperationException();
+ }
+
+ public KernelEventHandler unregisterKernelEventHandler( KernelEventHandler kernelEventHandler ) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public TransactionBuilder tx() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void shutdown() {
+ }
+}
diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/CypherRestShell.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/CypherRestShell.java
new file mode 100644
index 000000000..fecd8a64c
--- /dev/null
+++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/CypherRestShell.java
@@ -0,0 +1,55 @@
+/**
+ * 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.rest.graphdb.query.CypherRestResult;
+import org.neo4j.rest.graphdb.query.CypherResult;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.List;
+
+public class CypherRestShell {
+ public static void main(String[] args) throws IOException {
+ String uri = (args.length>0) ? args[0] : "http://localhost:7474/db/data";
+ RestAPIImpl restAPIFacade = args.length>1 ? new RestAPIImpl(uri,args[1],args[2]) : new RestAPIImpl(uri);
+ System.out.println("Connected to "+uri);
+ try {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
+ String query;
+ System.out.print("Query: ");
+ while ((query=reader.readLine())!=null && !query.isEmpty()) {
+ long time=System.currentTimeMillis();
+ CypherResult result = restAPIFacade.query(query, null);
+ time=System.currentTimeMillis()-time;
+ System.out.println(result.getColumns());
+ List> rows = (List>) result.getData();
+ for (List row : rows) {
+ System.out.println(row);
+ }
+ System.out.println(rows.size()+" row(s), roundtrip time "+time+" ms.");
+ System.out.print("Query: ");
+ }
+ } finally {
+ restAPIFacade.close();
+ }
+ }
+}
diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/ExecutingRestRequest.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/ExecutingRestRequest.java
new file mode 100644
index 000000000..1bfd8e4e7
--- /dev/null
+++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/ExecutingRestRequest.java
@@ -0,0 +1,184 @@
+/**
+ * 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 com.sun.jersey.api.client.Client;
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.client.WebResource;
+import com.sun.jersey.api.client.WebResource.Builder;
+import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter;
+import com.sun.jersey.api.client.filter.LoggingFilter;
+import org.neo4j.helpers.collection.MapUtil;
+import org.neo4j.rest.graphdb.util.Config;
+import org.neo4j.rest.graphdb.util.JsonHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.core.MediaType;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.util.Map;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
+
+public class ExecutingRestRequest implements RestRequest {
+
+ private static final Logger log = LoggerFactory.getLogger(RestRequest.class);
+
+ public static final MediaType STREAMING_JSON_TYPE = new MediaType(APPLICATION_JSON_TYPE.getType(),APPLICATION_JSON_TYPE.getSubtype(), MapUtil.stringMap("stream","true"));
+ private final String baseUri;
+ private final UserAgent userAgent = new UserAgent();
+ private final Client client;
+
+ public ExecutingRestRequest( String baseUri ) {
+ this( baseUri, null, null );
+ }
+
+ public ExecutingRestRequest( String baseUri, String username, String password ) {
+ this.baseUri = uriWithoutSlash( baseUri );
+ client = createClient();
+ addAuthFilter(username, password);
+
+ }
+
+ protected void addAuthFilter(String username, String password) {
+ if (username == null) return;
+ client.addFilter( new HTTPBasicAuthFilter( username, password ) );
+ }
+
+ protected Client createClient() {
+ Client client = Client.create();
+ client.setConnectTimeout(Config.getConnectTimeout());
+ client.setReadTimeout(Config.getReadTimeout());
+ client.setChunkedEncodingSize(8*1024);
+ userAgent.install(client);
+ if (Config.useLoggingFilter()) {
+ client.addFilter(new LoggingFilter());
+ }
+ return client;
+ }
+
+ private ExecutingRestRequest( String uri, Client client ) {
+ this.baseUri = uriWithoutSlash( uri );
+ this.client = client;
+ }
+
+ protected String uriWithoutSlash( String uri ) {
+ return (uri.endsWith("/") ? uri.substring(0, uri.length() - 1) : uri);
+ }
+
+ public static String encode( Object value ) {
+ if ( value == null ) return "";
+ try {
+ return URLEncoder.encode( value.toString(), "utf-8" ).replaceAll( "\\+", "%20" );
+ } catch ( UnsupportedEncodingException e ) {
+ throw new RuntimeException( e );
+ }
+ }
+
+
+ private Builder builder( String path ) {
+ WebResource resource = client.resource( uri( pathOrAbsolute( path ) ) );
+ if (Config.streamingIsEnabled()) return resource.accept(STREAMING_JSON_TYPE).header("X-Stream","true");
+ return resource.accept(APPLICATION_JSON_TYPE);
+ }
+
+ private String pathOrAbsolute( String path ) {
+ if ( path.startsWith( "http://" ) ) return path;
+ return baseUri + "/" + path;
+ }
+
+
+ @Override
+ public RequestResult get( String path ) {
+ if (log.isDebugEnabled()) log.debug("GET "+path);
+ return RequestResult.extractFrom(builder(path).get(ClientResponse.class));
+ }
+
+
+ @Override
+ public RequestResult get( String path, Object data ) {
+ Builder builder = builder(path);
+ if ( data != null ) {
+ builder = builder.entity( JsonHelper.createJsonFrom( data ), APPLICATION_JSON_TYPE );
+ }
+ if (log.isDebugEnabled()) log.debug("GET "+path+" "+data);
+ return RequestResult.extractFrom(builder.get(ClientResponse.class));
+ }
+
+
+ @Override
+ public RequestResult delete(String path) {
+ if (log.isDebugEnabled()) log.debug("DELETE "+path);
+ return RequestResult.extractFrom(builder(path).delete(ClientResponse.class));
+ }
+
+
+ @Override
+ public RequestResult post( String path, Object data ) {
+ Builder builder = builder( path );
+ if ( data != null ) {
+ Object payload = data instanceof InputStream ? data : JsonHelper.createJsonFrom(data);
+ builder = builder.entity( payload , APPLICATION_JSON_TYPE );
+ }
+ if (log.isDebugEnabled()) log.debug("POST "+path+" "+data);
+ return RequestResult.extractFrom(builder.post(ClientResponse.class));
+ }
+
+ @Override
+ public RequestResult put( String path, Object data ) {
+ Builder builder = builder( path );
+ if ( data != null ) {
+ builder = builder.entity( JsonHelper.createJsonFrom( data ), APPLICATION_JSON_TYPE );
+ }
+ if (log.isDebugEnabled()) log.debug("PUT "+path+" "+data);
+ return RequestResult.extractFrom(builder.put(ClientResponse.class));
+ }
+
+ @Override
+ public RestRequest with( String uri ) {
+ return new ExecutingRestRequest(uri, client);
+ }
+
+ private URI uri( String uri ) {
+ try {
+ return new URI( uri );
+ } catch ( URISyntaxException e ) {
+ throw new RuntimeException( e );
+ }
+ }
+
+
+ @Override
+ public String getUri() {
+ return baseUri;
+ }
+
+ @Override
+ public Map, ?> toMap(RequestResult requestResult) {
+ return requestResult.toMap();
+ }
+
+ public static void shutdown() {
+ }
+}
diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/PropertiesMap.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/PropertiesMap.java
new file mode 100644
index 000000000..d5945f3c4
--- /dev/null
+++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/PropertiesMap.java
@@ -0,0 +1,159 @@
+/**
+ * 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;
+
+/**
+ * @author mh
+ * @since 13.12.10
+ */
+
+import org.neo4j.graphdb.PropertyContainer;
+
+import java.lang.reflect.Array;
+import java.util.*;
+
+public class PropertiesMap {
+
+ private final Map values = new HashMap();
+
+ public PropertiesMap( PropertyContainer container ) {
+ for ( String key : container.getPropertyKeys() ) {
+ values.put( key, container.getProperty( key ) );
+ }
+ }
+
+ public PropertiesMap( Map map ) {
+ for ( Map.Entry entry : map.entrySet() ) {
+ values.put( entry.getKey(), toInternalType( entry.getValue() ) );
+ }
+ }
+
+ public Object getValue( String key ) {
+ return values.get( key );
+ }
+
+ public Map serialize() {
+ // TODO Nice with sorted, but TreeMap the best?
+ Map result = new TreeMap();
+ for ( Map.Entry entry : values.entrySet() ) {
+ result.put( entry.getKey(), toSerializedType( entry.getValue() ) );
+ }
+ return result;
+ }
+
+ void storeTo( PropertyContainer container ) {
+ for ( Map.Entry entry : values.entrySet() ) {
+ container.setProperty( entry.getKey(), entry.getValue() );
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Object toInternalType( Object value ) {
+ if ( value instanceof List ) {
+ List list = (List) value;
+ if ( list.isEmpty() ) {
+ return new byte[0];
+ } else {
+ Object first = list.get( 0 );
+ if ( first instanceof String ) {
+ return stringArray( list );
+ } else if ( first instanceof Number ) {
+ return numberArray( list );
+ } else if ( first instanceof Boolean ) {
+ return booleanArray( list );
+ } else {
+ throw new RuntimeException( "Unsupported array type " + first.getClass() +
+ ". Supported array types are arrays of all java primitives (" +
+ "byte[], char[], short[], int[], long[], float[], double[]) " +
+ "and String[]" );
+ }
+ }
+ } else {
+ return assertSupportedPropertyValue( value );
+ }
+ }
+
+ public static Object assertSupportedPropertyValue( Object value ) {
+ if ( value == null ) {
+ throw new RuntimeException( "null value not supported" );
+ }
+ final Class> type = value.getClass();
+ if (isSupportedType(type) || type.isArray() && isSupportedType(type.getComponentType())) {
+ return value;
+ }
+ throw new RuntimeException( "Unsupported value type " + type + "." +
+ " Supported value types are all java primitives (byte, char, short, int, " +
+ "long, float, double) and String, as well as arrays of all those types" );
+ }
+
+ private static boolean isSupportedType(Class> type) {
+ return type.isPrimitive() || String.class.isAssignableFrom(type) || Number.class.isAssignableFrom(type) || Boolean.class.isAssignableFrom(type);
+ }
+
+ private static Boolean[] booleanArray( List list ) {
+ return list.toArray( new Boolean[list.size()] );
+ }
+
+ private static Number[] numberArray( List numbers ) {
+ Number[] internal = new Number[numbers.size()];
+ for ( int i = 0; i < internal.length; i++ ) {
+ Number number = numbers.get( i );
+ if ( number instanceof Float || number instanceof Double ) {
+ number = number.doubleValue();
+ } else {
+ number = number.longValue();
+ }
+ internal[i] = number;
+ }
+ final Number[] result;
+ if ( internal[0] instanceof Double ) {
+ result = new Double[internal.length];
+ } else {
+ result = new Long[internal.length];
+ }
+ System.arraycopy( internal, 0, result, 0, internal.length );
+ return result;
+ }
+
+ private static String[] stringArray( List strings ) {
+ return strings.toArray( new String[strings.size()] );
+ }
+
+ private Object toSerializedType( Object value ) {
+ if ( value.getClass().isArray() ) {
+ if ( value.getClass().getComponentType().isPrimitive() ) {
+ int size = Array.getLength( value );
+ List result = new ArrayList();
+ for ( int i = 0; i < size; i++ ) {
+ result.add( Array.get( value, i ) );
+ }
+ return result;
+ } else {
+ return Arrays.asList( (Object[]) value );
+ }
+ } else {
+ return value;
+ }
+ }
+
+ public boolean isEmpty() {
+ return values.isEmpty();
+ }
+}
\ No newline at end of file
diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RequestResult.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RequestResult.java
new file mode 100644
index 000000000..0fad9b886
--- /dev/null
+++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RequestResult.java
@@ -0,0 +1,148 @@
+/**
+ * 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 java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.Map;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.StatusType;
+
+import org.neo4j.rest.graphdb.util.JsonHelper;
+
+import com.sun.jersey.api.client.ClientResponse;
+import org.neo4j.rest.graphdb.util.StreamJsonHelper;
+
+
+/**
+* @author Klemens Burchardi
+* @since 03.08.11
+*/
+public class RequestResult {
+ private final int status;
+ private final String location;
+ private ClientResponse response;
+ private String string;
+ private Object entity;
+ private InputStream stream;
+
+ RequestResult(int status, String location, String string) {
+ this.status = status;
+ this.location = location;
+ this.string = string;
+ this.stream = null;
+ }
+
+ RequestResult(int status, String location, InputStream stream, ClientResponse response) {
+ this.status = status;
+ this.location = location;
+ this.response = response;
+ this.entity = null;
+ this.stream = stream;
+ }
+
+ public static RequestResult extractFrom(ClientResponse clientResponse) {
+ final int status = clientResponse.getStatus();
+ final URI location = clientResponse.getLocation();
+ // final InputStream data;
+ if (status == Response.Status.NO_CONTENT.getStatusCode()) {
+ // data = null;
+ clientResponse.close();
+ return new RequestResult(status, uriString(location), null,clientResponse);
+ } else {
+ // data = clientResponse.getEntityInputStream();
+ RequestResult result = new RequestResult(status, uriString(location), clientResponse.getEntity(String.class));
+ clientResponse.close();
+ return result;
+ }
+ //return new RequestResult(status, uriString(location), data,clientResponse);
+ }
+
+ public static RequestResult extractFrom(Map batchResult) {
+ return new RequestResult(200, (String) batchResult.get("location"),JsonHelper.createJsonFrom(batchResult.get("body")));
+ }
+
+ private static String uriString(URI location) {
+ return location==null ? null : location.toString();
+ }
+
+
+ public int getStatus() {
+ return status;
+ }
+
+ public String getLocation() {
+ return location;
+ }
+
+ public Object toEntity() {
+ if (entity!=null) return entity;
+ if (stream != null) {
+ entity = StreamJsonHelper.jsonToSingleValue(stream);
+ closeStream();
+ }
+ else {
+ entity = JsonHelper.jsonToSingleValue(string);
+ }
+ return entity;
+ }
+
+ public boolean isMap() {
+ return toEntity() instanceof Map;
+ }
+ public Map, ?> toMap() {
+ return (Map, ?>) toEntity();
+ }
+
+ public boolean statusIs( StatusType status ) {
+ return getStatus() == status.getStatusCode();
+ }
+
+ public boolean statusOtherThan( StatusType status ) {
+ return !statusIs(status );
+ }
+
+ public String getText() {
+ if (string==null && stream!=null) {
+ string = JsonHelper.readString(stream);
+ closeStream();
+ }
+ return string;
+ }
+
+ private void closeStream() {
+ if (stream!=null) readFully(stream);
+ stream = null;
+ if (response!=null) {
+ response.close();
+ response = null;
+ }
+ }
+
+ private void readFully(InputStream stream) {
+ try {
+ while (stream.read()!=-1);
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+}
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
new file mode 100644
index 000000000..120192a0d
--- /dev/null
+++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPI.java
@@ -0,0 +1,92 @@
+/**
+ * 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.traversal.TraversalDescription;
+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.RestTraverser;
+import org.neo4j.rest.graphdb.util.QueryResult;
+import org.neo4j.rest.graphdb.util.ResultConverter;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * @author mh
+ * @since 02.05.12
+ */
+public interface RestAPI extends RestAPIIndex, RestAPIInternal {
+
+ void deleteEntity(RestEntity entity);
+
+ void setPropertyOnEntity(RestEntity entity, String key, Object value);
+ void setPropertiesOnEntity(RestEntity restEntity, Map propertyData);
+// Map,?> getData(RestEntity uri);
+ void removeProperty(RestEntity entity, String key);
+
+ RestNode getNodeById(long id);
+
+ RestNode createNode(Map props);
+ RestNode createNode(Map props,Collection labels);
+
+ RestRelationship getRelationshipById(long id);
+ RestRelationship createRelationship(Node startNode, Node endNode, RelationshipType type, Map props);
+
+ Iterable getRelationshipTypes(RestNode node);
+ int getDegree(RestNode restNode, RelationshipType type, Direction direction);
+
+ void addLabels(RestNode node, Collection labels);
+ void removeLabel(RestNode node, String label);
+
+ Iterable getNodesByLabel(String label);
+ Iterable getNodesByLabelAndProperty(String label, String property, Object value);
+
+ org.neo4j.rest.graphdb.query.CypherResult query(String statement, Map params);
+ QueryResult> query(String statement, Map params, ResultConverter resultConverter);
+
+ Transaction beginTx();
+
+ Collection getAllLabelNames();
+
+ Iterable getRelationshipTypes();
+
+ TraversalDescription createTraversalDescription();
+
+ Iterable getRelationships(RestNode restNode, Direction direction, RelationshipType... types);
+
+ RestTraverser traverse(RestNode restNode, Map description);
+
+ RestNode merge(String labelName, String key, Object value, Map properties, Collection labels);
+
+ RequestResult batch(Collection> batchRequestData);
+
+ // internal
+
+ RestRequest getRestRequest();
+
+ RestNode addToCache(RestNode restNode);
+ RestNode getFromCache(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
new file mode 100644
index 000000000..b5f16f5c5
--- /dev/null
+++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPICypherImpl.java
@@ -0,0 +1,553 @@
+/**
+ * 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.index.IndexHits;
+import org.neo4j.graphdb.traversal.TraversalDescription;
+import org.neo4j.helpers.collection.IterableWrapper;
+import org.neo4j.helpers.collection.IteratorUtil;
+import org.neo4j.helpers.collection.MapUtil;
+import org.neo4j.index.impl.lucene.AbstractIndexHits;
+import org.neo4j.rest.graphdb.converter.RestEntityExtractor;
+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.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.transaction.RemoteCypherTransaction;
+import org.neo4j.rest.graphdb.traversal.RestTraverser;
+import org.neo4j.rest.graphdb.util.QueryResult;
+import org.neo4j.rest.graphdb.util.ResultConverter;
+
+import javax.ws.rs.core.Response.Status;
+import java.util.*;
+
+import static org.neo4j.helpers.collection.MapUtil.map;
+
+
+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 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}) ";
+ public static final String GET_REL_QUERY = _MATCH_REL_QUERY + _QUERY_RETURN_REL;
+
+ public static final String GET_REL_TYPES_QUERY = _MATCH_NODE_QUERY + " MATCH (n)-[r]-() RETURN distinct type(r) as relType";
+
+ private String createNodeQuery(Collection labels) {
+ String labelString = toLabelString(labels);
+ return "CREATE (n" + labelString + " {props}) " + _QUERY_RETURN_NODE;
+ }
+
+ private String mergeQuery(String labelName, String key, Collection labels) {
+ StringBuilder setLabels = new StringBuilder();
+ 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;
+ }
+
+ private String toLabelString(Collection labels) {
+ if (labels==null || labels.size() == 0) return "";
+ StringBuilder sb = new StringBuilder();
+ for (String label : labels) {
+ sb.append(":").append(label);
+ }
+ return sb.toString();
+ }
+
+ private final RestAPI restAPI;
+ private RestRequest restRequest;
+
+ private static final ThreadLocal cypherTransaction = new ThreadLocal<>();
+
+ protected RestAPICypherImpl(RestAPI restAPI) {
+ this.restAPI = restAPI;
+ this.restRequest = restAPI.getRestRequest();
+ }
+
+ @Override
+ public RestNode getNodeById(long id, Load force) {
+ if (force != Load.ForceFromServer) {
+ RestNode restNode = getFromCache(id);
+ if (restNode != null) return restNode;
+ }
+ if (force == Load.FromCache) return new RestNode(RestNode.nodeUri(this, id),this);
+ Iterator> result = query(GET_NODE_QUERY, map("id", id)).getData().iterator();
+ if (!result.hasNext()) {
+ throw new NotFoundException("Node not found " + id);
+ }
+ List row = result.next();
+ return addToCache(toNode(row));
+ }
+
+ public RestNode getFromCache(long id) {
+ return restAPI.getFromCache(id);
+ }
+
+ @Override
+ public RestNode getNodeById(long id) {
+ return getNodeById(id, Load.FromServer);
+ }
+
+ private RestNode toNode(List row) {
+ long id = ((Number) row.get(0)).longValue();
+ List labels = (List) row.get(1);
+ 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);
+ long start = ((Number) row.get(3)).longValue();
+ long end = ((Number) row.get(4)).longValue();
+ 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);
+ }
+ List row = result.next();
+ return toRel(row);
+ }
+
+
+ @Override
+ public RestNode createNode(Map props) {
+ return createNode(props,Collections.emptyList());
+ }
+ @Override
+ public RestNode createNode(Map props, Collection labels) {
+ Map, Object> data = props == null ? Collections.emptyMap() : props;
+ Iterator> result = query(createNodeQuery(labels), map("props", data)).getData().iterator();
+ if (result.hasNext()) {
+ return addToCache(toNode(result.next()));
+ }
+ throw new RuntimeException("Error creating node with labels: " + labels + " and props: " + props + " no data returned");
+ }
+
+ @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");
+ 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");
+
+ return addToCache(toNode(result.next()));
+ }
+
+ public RestNode addToCache(RestNode restNode) {
+ return restAPI.addToCache(restNode);
+ }
+
+ @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);
+ CypherTransaction.Result result = runQuery(statement, params);
+ 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());
+ }
+
+
+ @Override
+ 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);
+ }
+ }
+
+ @Override
+ public Iterable getNodesByLabel(String label) {
+ String statement = "MATCH (n:`" + label + "`) " + _QUERY_RETURN_NODE;
+ return queryForNodes(statement, null);
+ }
+
+ private Iterable queryForNodes(String statement, Map params) {
+ Iterable> result = runQuery(statement, params).getRows();
+ return new IterableWrapper>(result) {
+ protected RestNode underlyingObjectToObject(List row) {
+ return addToCache(toNode(row));
+ }
+ };
+ }
+
+ @Override
+ public Iterable getNodesByLabelAndProperty(String label, String property, Object value) {
+ String statement = "MATCH (n:`" + label + "`) WHERE n.`"+property+"` = {value} " + _QUERY_RETURN_NODE;
+ return queryForNodes(statement, map("value", value));
+ }
+
+ @Override
+ public Iterable getRelationshipTypes(RestNode node) {
+ Iterable> result = runQuery(GET_REL_TYPES_QUERY, map("id", node.getId())).getRows();
+ return new IterableWrapper>(result) {
+ protected RelationshipType underlyingObjectToObject(List row) {
+ return DynamicRelationshipType.withName(row.get(0).toString());
+ }
+ };
+ }
+
+ @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";
+ Iterator> degree = runQuery(nodeDegreeQuery, map("id", restNode.getId())).getRows().iterator();
+ if (!degree.hasNext()) return 0;
+ 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+"]-";
+ if (direction == Direction.OUTGOING) {
+ relPattern += ">";
+ } else if (direction == Direction.INCOMING) {
+ relPattern = "<" + relPattern;
+ }
+ return relPattern;
+ }
+
+ private String toTypeString(RelationshipType... types) {
+ if (types==null || types.length == 0) return "";
+ StringBuilder typeString = new StringBuilder();
+ for (RelationshipType type : types) {
+ if (typeString.length() > 0 ) typeString.append("|");
+ typeString.append(':').append('`').append(type.name()).append("`");
+ }
+ return typeString.toString();
+ }
+
+ @Override
+ public Iterable getRelationships(RestNode restNode, Direction direction, RelationshipType... types) {
+ 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()) {
+ protected Relationship underlyingObjectToObject(List row) {
+ return toRel(row);
+ }
+ };
+ }
+
+ @Override
+ public void addLabels(RestNode node, Collection labels) {
+ String statement = _MATCH_NODE_QUERY + " SET n"+toLabelString(labels) + _QUERY_RETURN_NODE;
+ runQuery(statement,map("id",node.getId()));
+ RequestResult response = getRestRequest().with(node.getUri()).post("labels", labels);
+
+ if (response.statusOtherThan(Status.NO_CONTENT)) {
+ throw new IllegalStateException("error adding labels, received " + response);
+ }
+ }
+
+ public RestRequest getRestRequest() {
+ return restRequest;
+ }
+
+ @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);
+ }
+ }
+
+ @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;
+ CypherTransaction.Result result = runQuery(statement, map("query", value));
+ return toIndexHits(result,true);
+ }
+ if (Relationship.class.isAssignableFrom(entityType)) {
+ String statement = "start r=rel"+index+ _QUERY_RETURN_REL;
+ CypherTransaction.Result result = runQuery(statement, map("query", value));
+ return toIndexHits(result,false);
+ }
+ throw new IllegalStateException("Unknown index entity type "+entityType);
+ }
+
+ @Override
+ public IndexHits queryIndex(Class entityType, String indexName, String key, Object value) {
+ String index = ":`" + indexName + "`({query})";
+ if (Node.class.isAssignableFrom(entityType)) {
+ String statement = "start n=node"+index+ _QUERY_RETURN_NODE;
+ CypherTransaction.Result result = runQuery(statement, map("query", value));
+ return toIndexHits(result,true);
+ }
+ if (Relationship.class.isAssignableFrom(entityType)) {
+ String statement = "start r=rel"+index+ _QUERY_RETURN_REL;
+ CypherTransaction.Result result = runQuery(statement, map("query", value));
+ return toIndexHits(result,false);
+ }
+ throw new IllegalStateException("Unknown index entity type "+entityType);
+ }
+
+ private IndexHits toIndexHits(CypherTransaction.Result result, final boolean isNode) {
+ final int size = IteratorUtil.count(result.getRows());
+ final Iterator> it = result.getRows().iterator();
+ return new AbstractIndexHits() {
+ @Override
+ public int size() {
+ return size;
+ }
+
+ @Override
+ public float currentScore() {
+ return 0;
+ }
+
+ @Override
+ protected S fetchNextOrNull() {
+ if (!it.hasNext()) return null;
+ return (S)(isNode ? addToCache(toNode(it.next())) : toRel(it.next()));
+ }
+ };
+ }
+
+ @Override
+ public RestIndexManager index() {
+ return restAPI.index();
+ }
+
+
+ @Override
+ public void deleteEntity(RestEntity entity) {
+ if (entity instanceof Node) {
+ runQuery(_MATCH_NODE_QUERY + " DELETE n", map("id", entity.getId()));
+ } else if (entity instanceof Relationship) {
+ 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));
+ } else if (entity instanceof Relationship) {
+ runQuery(_MATCH_REL_QUERY + " SET r.`"+key+"` = {value} ", map("id", entity.getId(), "value", value));
+ }
+ }
+
+ // TODO return entity ???
+ @Override
+ public void setPropertiesOnEntity(RestEntity entity, Map properties) {
+ if (entity instanceof Node) {
+ runQuery(_MATCH_NODE_QUERY + " SET n = {props} ", map("id", entity.getId(), "props", properties));
+ } else if (entity instanceof Relationship) {
+ runQuery(_MATCH_REL_QUERY + " SET r = {props} ", map("id", entity.getId(), "props", properties));
+ }
+ }
+
+ @Override
+ public void removeProperty(RestEntity entity, String key) {
+ if (entity instanceof Node) {
+ 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()));
+ }
+ }
+
+ // 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);
+ }
+
+ // 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);
+ }
+
+ public CypherResult query(String statement, Map params) {
+ return new CypherTxResult(runQuery(statement, params));
+ }
+
+ private CypherTransaction.Result runQuery(String statement, Map params) {
+ if (cypherTransaction.get() == null) {
+ return newCypherTransaction().commit(statement,params);
+ }
+ return cypherTransaction.get().send(statement,params);
+ }
+
+ private 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);
+ }
+
+ @Override
+ public RestTraverser traverse(RestNode restNode, Map description) {
+ return restAPI.traverse(restNode, description);
+ }
+
+ public RequestResult batch(Collection> batchRequestData) {
+ return restAPI.batch(batchRequestData);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public RestIndex getIndex(String indexName) {
+ return restAPI.getIndex(indexName);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void createIndex(String type, String indexName, Map config) {
+ restAPI.createIndex(type, indexName, config);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public RestIndex createIndex(Class type, String indexName, Map config) {
+ return restAPI.createIndex(type,indexName,config);
+ }
+
+ @Override
+ public void close() {
+ restAPI.close();
+ }
+
+ @Override
+ public boolean isAutoIndexingEnabled(Class extends PropertyContainer> clazz) {
+ return restAPI.isAutoIndexingEnabled(clazz);
+ }
+
+ @Override
+ public void setAutoIndexingEnabled(Class extends PropertyContainer> clazz, boolean enabled) {
+ restAPI.setAutoIndexingEnabled(clazz, enabled);
+ }
+
+ @Override
+ public Set getAutoIndexedProperties(Class forClass) {
+ return restAPI.getAutoIndexedProperties(forClass);
+ }
+
+ @Override
+ public void startAutoIndexingProperty(Class forClass, String s) {
+ restAPI.startAutoIndexingProperty(forClass,s);
+ }
+
+ @Override
+ public void stopAutoIndexingProperty(Class forClass, String s) {
+ restAPI.stopAutoIndexingProperty(forClass, s);
+ }
+
+ @Override
+ public void delete(RestIndex index) {
+ restAPI.delete(index);
+ }
+
+ @Override
+ public void removeFromIndex(RestIndex index, T entity, String key, Object value) {
+ restAPI.removeFromIndex(index, entity, key, value);
+ }
+
+ @Override
+ public void removeFromIndex(RestIndex index, T entity, String key) {
+ restAPI.removeFromIndex(index,entity,key);
+ }
+
+ @Override
+ public void removeFromIndex(RestIndex index, T entity) {
+ restAPI.removeFromIndex(index,entity);
+ }
+
+
+ @Override
+ public void addToIndex(T entity, RestIndex index, String key, Object value) {
+ restAPI.addToIndex(entity,index,key,value);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public T putIfAbsent(T entity, RestIndex index, String key, Object value) {
+ return restAPI.putIfAbsent(entity, index, key, value);
+ }
+
+ @Override
+ public boolean hasToUpdate(long lastUpdate) {
+ return restAPI.hasToUpdate(lastUpdate);
+ }
+ @Override
+ public IndexInfo indexInfo(final String indexType) {
+ return restAPI.indexInfo(indexType);
+ }
+
+
+ @Override
+ public Collection getAllLabelNames() {
+ return restAPI.getAllLabelNames();
+ }
+
+ @Override
+ public Iterable getRelationshipTypes() {
+ return restAPI.getRelationshipTypes();
+ }
+
+ @Override
+ public TraversalDescription createTraversalDescription() {
+ return restAPI.createTraversalDescription();
+ }
+
+ public String getBaseUri() {
+ return restRequest.getUri();
+ }
+
+ @Override
+ public RestEntityExtractor getEntityExtractor() {
+ return restAPI.getEntityExtractor();
+ }
+
+ @Override
+ public RestEntity createRestEntity(Map data) {
+ return restAPI.createRestEntity(data);
+ }
+}
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
new file mode 100644
index 000000000..4f8738379
--- /dev/null
+++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIImpl.java
@@ -0,0 +1,717 @@
+/**
+ * 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.index.IndexHits;
+import org.neo4j.graphdb.traversal.TraversalDescription;
+import org.neo4j.helpers.collection.IterableWrapper;
+import org.neo4j.helpers.collection.MapUtil;
+import org.neo4j.index.lucene.ValueContext;
+import org.neo4j.rest.graphdb.batch.BatchRestAPI;
+import org.neo4j.rest.graphdb.entity.RestEntityCache;
+import org.neo4j.rest.graphdb.query.CypherRestResult;
+import org.neo4j.rest.graphdb.converter.RelationshipIterableConverter;
+import org.neo4j.rest.graphdb.converter.RestEntityExtractor;
+import org.neo4j.rest.graphdb.converter.RestIndexHitsConverter;
+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.index.IndexInfo;
+import org.neo4j.rest.graphdb.index.RestIndex;
+import org.neo4j.rest.graphdb.index.RestIndexManager;
+import org.neo4j.rest.graphdb.index.RetrievedIndexInfo;
+import org.neo4j.rest.graphdb.index.SimpleIndexHits;
+import org.neo4j.rest.graphdb.query.CypherResult;
+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.RestTraverser;
+import org.neo4j.rest.graphdb.util.JsonHelper;
+import org.neo4j.rest.graphdb.util.QueryResult;
+import org.neo4j.rest.graphdb.util.ResultConverter;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+import static javax.ws.rs.core.Response.Status.CREATED;
+import static org.neo4j.helpers.collection.MapUtil.map;
+import static org.neo4j.rest.graphdb.ExecutingRestRequest.encode;
+
+
+public class RestAPIImpl 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 GET_REL_TYPES_QUERY = "MATCH (n)-[r]-() WHERE id(n) = {id} RETURN distinct type(r) as relType";
+ private static final String[] NO_LABELS = new String[0];
+ protected RestRequest restRequest;
+
+ private long entityRefetchTimeInMillis = TimeUnit.SECONDS.toMillis(1000); //TODO move to cache
+ private final RestEntityCache entityCache = new RestEntityCache(this);
+ private RestEntityExtractor restEntityExtractor = new RestEntityExtractor(this);
+
+ public RestAPIImpl(String uri) {
+ this.restRequest = createRestRequest(uri, null, null);
+ }
+
+ public RestAPIImpl(String uri, String user, String password) {
+ this.restRequest = createRestRequest(uri, user, password);
+ }
+
+ protected RestRequest createRestRequest(String uri, String user, String password) {
+ return new ExecutingRestRequest(uri, user, password);
+ }
+
+ @Override
+ public RestIndexManager index() {
+ return new RestIndexManager(this);
+ }
+
+ @Override
+ public RestNode getNodeById(long id, Load force) {
+ if (force != Load.ForceFromServer) {
+ RestNode restNode = entityCache.getNode(id);
+ if (restNode != null) return restNode;
+ }
+ 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);
+ return entityCache.addToCache(node);
+ }
+
+ @Override
+ public RestNode addToCache(RestNode restNode) {
+ return entityCache.addToCache(restNode);
+ }
+
+ @Override
+ public RestNode getFromCache(long id) {
+ return entityCache.getNode(id);
+ }
+
+ @Override
+ public RestNode getNodeById(long id) {
+ return getNodeById(id, Load.FromServer);
+ }
+
+ @Override
+ public RestRelationship getRelationshipById(long id) {
+ RequestResult requestResult = restRequest.get("relationship/" + id);
+ if (requestResult.statusIs(Status.NOT_FOUND)) {
+ throw new NotFoundException("" + id);
+ }
+ return new RestRelationship(requestResult.toMap(), this);
+ }
+
+
+ @Override
+ public RestNode createNode(Map props) {
+ return createNode(props,Collections.emptyList());
+ }
+
+ @Override
+ public RestNode createNode(Map props, Collection labels) {
+ RequestResult result = restRequest.post("node", props);
+ RestNode node = createRestNode(result);
+ if (node==null) {
+ throw RestResultException.create(result);
+ }
+ addLabels(node,labels);
+ node.setLabels(labels);
+ return entityCache.addToCache(node);
+ }
+
+ @Override
+ public RestNode getOrCreateNode(RestIndex index, String key, Object value, final Map properties, Collection labels) {
+ if (index==null || key == null || value==null) throw new IllegalArgumentException("Unique index "+index+" key "+key+" value must not be null");
+ final Map data = map("key", key, "value", value, "properties", properties);
+ final RequestResult result = getRestRequest().post(uniqueIndexPath(index), data);
+ if (result.statusIs(Response.Status.CREATED) || result.statusIs(Response.Status.OK)) {
+ RestNode node = (RestNode) getEntityExtractor().convertFromRepresentation(result);
+ addLabels(node, labels);
+ node.setLabels(labels);
+ return entityCache.addToCache(node);
+ }
+ throw new RuntimeException(String.format("Error retrieving or creating node for key %s and value %s with index %s", key, value, index.getIndexName()));
+ }
+
+ private String toLabelString(String[] labels) {
+ if (labels==null || labels.length == 0) return "";
+ StringBuilder sb = new StringBuilder();
+ for (String label : labels) {
+ sb.append(":").append(label);
+ }
+ return sb.toString();
+ }
+
+
+ private RestNode createRestNode(RequestResult result) {
+ if (result.statusIs(Status.NOT_FOUND)) {
+ throw new NotFoundException("Node not found");
+ }
+ RestNode node = null;
+ if (result.statusIs(CREATED)) {
+ node = result.isMap() ? new RestNode(result.toMap(), this) : new RestNode(result.getLocation(), this);
+ }
+ if (node == null && result.statusIs(Status.OK)) {
+ node = new RestNode(result.toMap(), this);
+ }
+ return entityCache.addToCache(node);
+ }
+
+ @Override
+ public RestRelationship createRelationship(Node startNode, Node endNode, RelationshipType type, Map props) {
+ // final RestRequest restRequest = ((RestNode) startNode).getRestRequest();
+ final RestNode end = (RestNode) endNode;
+ Map data = map("to", end.getUri(), "type", type.name());
+ if (props != null && props.size() > 0) {
+ data.put("data", props);
+ }
+ final RestNode start = (RestNode) startNode;
+ RequestResult requestResult = getRestRequest().with(start.getUri()).post("relationships", data);
+ return createRestRelationship(requestResult, startNode);
+ }
+
+ private RestRelationship createRestRelationship(RequestResult requestResult, PropertyContainer element) {
+ if (requestResult.statusOtherThan(CREATED)) {
+ final int status = requestResult.getStatus();
+ throw new RuntimeException("Error creating relationship " + status+" "+requestResult.getText());
+ }
+ final String location = requestResult.getLocation();
+ if (requestResult.isMap()) return new RestRelationship(requestResult.toMap(), this);
+ return new RestRelationship(location, this);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public RestIndex getIndex(String indexName) {
+ final RestIndexManager index = this.index();
+ if (index.existsForNodes(indexName)) return (RestIndex) index.forNodes(indexName);
+ if (index.existsForRelationships(indexName)) return (RestIndex) index.forRelationships(indexName);
+ throw new IllegalArgumentException("Index " + indexName + " does not yet exist");
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void createIndex(String type, String indexName, Map config) {
+ Map data=new HashMap();
+ data.put("name",indexName);
+ data.put("config",config);
+ restRequest.post("index/" + type, data);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public RestIndex createIndex(Class type, String indexName, Map config) {
+ if (Node.class.isAssignableFrom(type)) {
+ return (RestIndex) index().forNodes( indexName, config);
+ }
+ if (Relationship.class.isAssignableFrom(type)) {
+ return (RestIndex) index().forRelationships(indexName, config);
+ }
+ throw new IllegalArgumentException("Required Node or Relationship types to create index, got " + type);
+ }
+
+ @Override
+ public void close() {
+ ExecutingRestRequest.shutdown();
+ }
+
+ @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");
+ 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");
+
+ return entityCache.addToCache(toNode(result.next()));
+ }
+
+ private String mergeQuery(String labelName, String key, Collection labels) {
+ StringBuilder setLabels = new StringBuilder();
+ 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;
+ }
+
+ @Override
+ public boolean isAutoIndexingEnabled(Class extends PropertyContainer> clazz) {
+ RequestResult response = getRestRequest().get(buildPathAutoIndexerStatus(clazz));
+ if (response.statusIs(Response.Status.OK)) {
+ return Boolean.parseBoolean(response.getText());
+ } else {
+ throw new IllegalStateException("received " + response);
+ }
+ }
+
+ @Override
+ public void setAutoIndexingEnabled(Class extends PropertyContainer> clazz, boolean enabled) {
+ RequestResult response = getRestRequest().put(buildPathAutoIndexerStatus(clazz), enabled);
+ if (response.statusOtherThan(Status.NO_CONTENT)) {
+ throw new IllegalStateException("received " + response);
+ }
+ }
+
+ @Override
+ public Set getAutoIndexedProperties(Class forClass) {
+ RequestResult response = getRestRequest().get(buildPathAutoIndexerProperties(forClass).toString());
+ Collection autoIndexedProperties = (Collection) JsonHelper.readJson(response.getText());
+ return new HashSet(autoIndexedProperties);
+ }
+
+ @Override
+ public void startAutoIndexingProperty(Class forClass, String s) {
+ try {
+ // we need to use a inputstream instead of the string directly. Otherwise "post" implicitly uses
+ // StreamJsonHelper.writeJsonTo which quotes a given string
+ InputStream stream = new ByteArrayInputStream(s.getBytes("UTF-8"));
+ RequestResult response = getRestRequest().post(buildPathAutoIndexerProperties(forClass).toString(), stream);
+ if (response.statusOtherThan(Status.NO_CONTENT)) {
+ throw new IllegalStateException("received " + response);
+ }
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalStateException(e);
+ }
+
+ }
+
+ @Override
+ public void stopAutoIndexingProperty(Class forClass, String s) {
+ RequestResult response = getRestRequest().delete(buildPathAutoIndexerProperties(forClass).append("/").append(s).toString());
+ if (response.statusOtherThan(Status.NO_CONTENT)) {
+ throw new IllegalStateException("received " + response);
+ }
+ }
+
+
+ @Override
+ public void removeLabel(RestNode node, String label) {
+ RequestResult response = getRestRequest().with(node.getUri()).delete("labels/" + encode(label));
+ if (response.statusOtherThan(Status.NO_CONTENT)) {
+ throw new IllegalStateException("received " + response);
+ }
+ }
+
+ public Collection getNodeLabels(long id) {
+ RequestResult response = restRequest.get(RestNode.nodeUri(this,id)+"/labels");
+ if (response.statusOtherThan(Status.OK)) {
+ throw new IllegalStateException("received " + response);
+ }
+ return (Collection) response.toEntity();
+ }
+
+ @Override
+ public Collection getAllLabelNames() {
+ RequestResult response = restRequest.get("labels");
+ if (response.statusOtherThan(Status.OK)) {
+ throw new IllegalStateException("received " + response);
+ }
+ return (Collection) response.toEntity();
+ }
+
+ @Override
+ public Iterable getNodesByLabel(String label) {
+ RequestResult response = getRestRequest().get("label/" + encode(label) + "/nodes");
+ if (response.statusOtherThan(Status.OK)) {
+ throw new IllegalStateException("received " + response);
+ }
+ return (Iterable) getEntityExtractor().convertFromRepresentation(response);
+ }
+
+ private RestNode toNode(List row) {
+ long id = ((Number) row.get(0)).longValue();
+ List labels = (List) row.get(1);
+ Map restData = (Map) row.get(2);
+ return new RestNode(id, labels, restData, this);
+ }
+
+ private Iterable queryForNodes(String statement, Map params) {
+ Iterable> result = query(statement, params).getData();
+ return new IterableWrapper>(result) {
+ protected RestNode underlyingObjectToObject(List row) {
+ return entityCache.addToCache(toNode(row));
+ }
+ };
+ }
+
+ @Override
+ public Iterable getNodesByLabelAndProperty(String label, String property, Object value) {
+ String statement = "MATCH (n:`" + label + "`) WHERE n.`"+property+"` = {value} " + _QUERY_RETURN_NODE;
+ return queryForNodes(statement, map("value", value));
+ }
+
+ @Override
+ public Iterable getRelationshipTypes(RestNode node) {
+ Iterable> result = query(GET_REL_TYPES_QUERY, map("id", node.getId())).getData();
+ return new IterableWrapper>(result) {
+ protected RelationshipType underlyingObjectToObject(List row) {
+ return DynamicRelationshipType.withName(row.get(0).toString());
+ }
+ };
+ }
+
+ @Override
+ public int getDegree(RestNode restNode, RelationshipType type, Direction direction) {
+ String relPattern = "--";
+ if (type != null) relPattern = "-[:`"+type+"`]-";
+ if (direction == Direction.OUTGOING) {
+ relPattern += ">";
+ } else if (direction == Direction.INCOMING) {
+ relPattern = "<" + relPattern;
+ }
+ String nodeDegreeQuery = "MATCH (n)" + relPattern + "() WHERE id(n) = {id} RETURN count(*) as degree";
+ Iterator> degree = query(nodeDegreeQuery, map("id", restNode.getId())).getData().iterator();
+ if (!degree.hasNext()) return 0;
+ return ((Number)degree.next().get(0)).intValue();
+ }
+
+ @Override
+ public Iterable getRelationshipTypes() {
+ Object result = restRequest.get("relationship/types").toEntity();
+ if (!(result instanceof Iterable)) throw new RuntimeException("Error loading relationship types");
+
+ return new IterableWrapper((Iterable) result) {
+ protected RelationshipType underlyingObjectToObject(Object type) {
+ return DynamicRelationshipType.withName(type.toString());
+ }
+ };
+ }
+
+ @Override
+ public void addLabels(RestNode node, Collection labels) {
+ if (labels == null || labels.isEmpty()) return;
+ RequestResult response = getRestRequest().with(node.getUri()).post("labels", labels);
+
+ if (response.statusOtherThan(Status.NO_CONTENT)) {
+ throw new IllegalStateException("error adding labels, received " + response);
+ }
+ }
+
+ private String buildPathAutoIndexerStatus(Class extends PropertyContainer> clazz) {
+ return buildPathAutoIndexerBase(clazz).append("/status").toString();
+ }
+
+ private StringBuilder buildPathAutoIndexerProperties(Class extends PropertyContainer> clazz) {
+ return buildPathAutoIndexerBase(clazz).append("/properties");
+ }
+
+ private StringBuilder buildPathAutoIndexerBase(Class extends PropertyContainer> clazz) {
+ return new StringBuilder().append("index/auto/").append(indexTypeName(clazz));
+ }
+
+
+ public RestRequest getRestRequest() {
+ return restRequest;
+ }
+
+
+ @Override
+ public TraversalDescription createTraversalDescription() {
+ return new RestTraversal();
+ }
+
+ @Override
+ public Transaction beginTx() {
+ return new NullTransaction();
+ }
+
+ public long getEntityRefetchTimeInMillis() {
+ return entityRefetchTimeInMillis;
+ }
+
+ public String getBaseUri() {
+ return restRequest.getUri();
+ }
+
+
+ public void setEntityRefetchTimeInMillis(long entityRefetchTimeInMillis) {
+ this.entityRefetchTimeInMillis = entityRefetchTimeInMillis;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Iterable wrapRelationships(RequestResult requestResult) {
+ return (Iterable) new RelationshipIterableConverter(this).convertFromRepresentation(requestResult);
+ }
+
+ public RestEntityExtractor getEntityExtractor() {
+ return restEntityExtractor;
+ }
+
+
+ public String indexPath( Class entityType, String indexName, String key, Object value ) {
+ String typeName = indexTypeName(entityType);
+ return "index/" + typeName + "/" + encode(indexName) + (key!=null? "/" + encode(key) :"") + (value!=null ? "/" + encode(value):"");
+ }
+
+ private String indexTypeName(Class entityType) {
+ return entityType.getSimpleName().toLowerCase();
+ }
+
+ public String indexPath( Class entityType, String indexName ) {
+ return "index/" + indexTypeName(entityType) + "/" + encode(indexName);
+ }
+
+ private String queryPath( Class entityType, String indexName, String key, Object value ) {
+ return indexPath( entityType, indexName, key,null) + "?query="+ encode(value);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public IndexHits getIndex(Class entityType, String indexName, String key, Object value) {
+ String indexPath = indexPath(entityType, indexName, key, value);
+ RequestResult response = restRequest.get(indexPath);
+ if (response.statusIs(Response.Status.OK)) {
+ return new RestIndexHitsConverter(this, entityType).convertFromRepresentation(response);
+ } else {
+ return new SimpleIndexHits(Collections.emptyList(), 0, entityType, this);
+ }
+ }
+ @Override
+ @SuppressWarnings("unchecked")
+ public IndexHits queryIndex(Class entityType, String indexName, String key, Object value) {
+ String indexPath = queryPath(entityType, indexName, key, value);
+ RequestResult response = restRequest.get(indexPath);
+ if (response.statusIs(Response.Status.OK)) {
+ return new RestIndexHitsConverter(this, entityType).convertFromRepresentation(response);
+ } else {
+ return new SimpleIndexHits(Collections.emptyList(), 0, entityType, this);
+ }
+ }
+
+ @Override
+ public void deleteEntity(RestEntity entity) {
+ getRestRequest().with(entity.getUri()).delete( "" );
+ entityCache.remove(entity.getId());
+ }
+ @Override
+ public IndexInfo indexInfo(final String indexType) {
+ RequestResult response = restRequest.get("index/" + encode(indexType));
+ return new RetrievedIndexInfo(response);
+ }
+
+ @Override
+ public void setPropertyOnEntity(RestEntity entity, String key, Object value) {
+ RequestResult result = getRestRequest().with(entity.getUri()).put("properties/" + encode(key), value);
+ if (result.statusOtherThan(Status.NO_CONTENT))
+ throw new RuntimeException("Error setting properties on entity "+entity+" properties "+key+" : "+value);
+ }
+
+ @Override
+ public void setPropertiesOnEntity(RestEntity entity, Map properties) {
+ RequestResult result = getRestRequest().with(entity.getUri()).put("properties", properties);
+ if (result.statusOtherThan(Status.NO_CONTENT))
+ throw new RuntimeException("Error setting properties on entity "+entity+" properties "+properties);
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map getPropertiesFromEntity(RestEntity entity){
+ RequestResult response = getRestRequest().with(entity.getUri()).get("properties");
+ Map properties;
+ boolean ok = response.statusIs( Status.OK );
+ if ( ok ) {
+ properties = (Map) response.toMap( );
+ } else {
+ properties = Collections.emptyMap();
+ }
+
+ return properties;
+ }
+
+ private void deleteIndex(String indexPath) {
+ getRestRequest().delete(indexPath);
+ }
+
+ @Override
+ public void delete(RestIndex index) {
+ deleteIndex(indexPath(index, null, null));
+ }
+
+ @Override
+ public void removeFromIndex(RestIndex index, T entity, String key, Object value) {
+ String indexPath = indexPath(index, key, value);
+ deleteIndex(indexPath(indexPath, entity));
+ }
+
+ protected String indexPath(String indexPath, T restEntity) {
+ return indexPath + "/" + ((RestEntity)restEntity).getId();
+ }
+
+ @Override
+ public void removeFromIndex(RestIndex index, T entity, String key) {
+ String indexPath = indexPath(index, key, null);
+ deleteIndex(indexPath(indexPath, entity));
+ }
+
+ private String indexPath(RestIndex index, String key, Object value) {
+ return indexPath(index.getEntityType(), index.getIndexName(), key, value);
+ }
+
+ @Override
+ public void removeFromIndex(RestIndex index, T entity) {
+ deleteIndex(indexPath(indexPath(index, null, null), entity));
+ }
+
+ public String uniqueIndexPath(RestIndex index) {
+ return indexPath(index,null,null) + "?uniqueness=get_or_create";
+ }
+
+ @Override
+ public void addToIndex(T entity, RestIndex index, String key, Object value) {
+ final RestEntity restEntity = (RestEntity) entity;
+ String uri = restEntity.getUri();
+ if (value instanceof ValueContext) {
+ value = ((ValueContext)value).getCorrectValue();
+ }
+ final Map data = map("key", key, "value", value, "uri", uri);
+ final RequestResult result = getRestRequest().post(indexPath(index, null, null), data);
+ if (result.statusOtherThan(Status.CREATED)) throw new RuntimeException(String.format("Error adding element %d %s %s to index %s", restEntity.getId(), key, value, index.getIndexName()));
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public T putIfAbsent(T entity, RestIndex index, String key, Object value) {
+ final RestEntity restEntity = (RestEntity) entity;
+ restEntity.flush();
+ String uri = restEntity.getUri();
+ if (value instanceof ValueContext) {
+ value = ((ValueContext)value).getCorrectValue();
+ }
+ final Map data = map("key", key, "value", value, "uri", uri);
+ final RequestResult result = getRestRequest().post(uniqueIndexPath(index), data);
+ if (result.statusIs(Response.Status.CREATED)) {
+ if (index.getEntityType().equals(Node.class)) return (T)createRestNode(result);
+ if (index.getEntityType().equals(Relationship.class)) return (T)createRestRelationship(result,restEntity);
+ }
+ if (result.statusIs(Response.Status.OK)) {
+ return (T) getEntityExtractor().convertFromRepresentation(result);
+ }
+ throw new RuntimeException(String.format("Error adding element %d %s %s to index %s", restEntity.getId(), key, value, index.getIndexName()));
+ }
+
+ @Override
+ public boolean hasToUpdate(long lastUpdate) {
+ return timeElapsed(lastUpdate, getEntityRefetchTimeInMillis());
+ }
+
+ @Override
+ public void removeProperty(RestEntity entity, String key) {
+ restRequest.with(entity.getUri()).delete("properties/" + encode(key));
+ }
+
+ private boolean timeElapsed( long since, long isItGreaterThanThis ) {
+ return System.currentTimeMillis() - since > isItGreaterThanThis;
+ }
+
+ @Override
+ public RestRelationship getOrCreateRelationship(RestIndex index, String key, Object value, final RestNode start, final RestNode end, final String type, final Map properties) {
+ if (index==null || key == null || value==null) throw new IllegalArgumentException("Unique index "+index+" key "+key+" value must not be null");
+ if (start == null || end == null || type == null) throw new IllegalArgumentException("Neither start, end nore type must be null");
+ final Map data = map("key", key, "value", value, "properties", properties, "start", start.getUri(), "end", end.getUri(), "type", type);
+ final RequestResult result = getRestRequest().post(uniqueIndexPath(index), data);
+ if (result.statusIs(Response.Status.CREATED) || result.statusIs(Response.Status.OK)) {
+ return (RestRelationship) getEntityExtractor().convertFromRepresentation(result);
+ }
+ throw new RuntimeException(String.format("Error retrieving or creating relationship for key %s and value %s with index %s", key, value, index.getIndexName()));
+ }
+
+ public org.neo4j.rest.graphdb.query.CypherResult query(String statement, Map params) {
+ params = (params==null) ? Collections.emptyMap() : params;
+ final RequestResult requestResult = getRestRequest().post("cypher", map("query", statement, "params", params));
+ return new CypherRestResult(requestResult);
+ }
+
+ @Override
+ public Iterable getRelationships(RestNode restNode, Direction direction, RelationshipType... types) {
+ String path = (types.length > 1) ? relPath(types) : relPath(direction,types.length == 0 ? null : types[0]);
+ return wrapRelationships(getRestRequest().with(restNode.getUri()).get(path));
+ }
+
+
+ private String relPath(RelationshipType... types) {
+ String path = "relationships/all/";
+ int counter = 0;
+ for ( RelationshipType type : types ) {
+ if ( counter++ > 0 ) {
+ path += "&";
+ }
+ path += encode(type.name());
+ }
+ return path;
+ }
+
+ private String relPath(Direction direction,RelationshipType type) {
+ String path = "relationships/" + RestDirection.from(direction).shortName;
+ return type == null ? path : path + "/" + encode(type.name());
+ }
+
+
+ private static final String FULLPATH = "fullpath";
+
+ @Override
+ public RestTraverser traverse(RestNode restNode, Map description) {
+ final RequestResult result = getRestRequest().with(restNode.getUri()).post("traverse/" + FULLPATH, description);
+ if (result.statusOtherThan(Response.Status.OK)) throw new RuntimeException(String.format("Error executing traversal: %d %s",result.getStatus(), description));
+ final Object col = result.toEntity();
+ if (!(col instanceof Collection)) throw new RuntimeException(String.format("Unexpected traversal result, %s instead of collection", col != null ? col.getClass() : null));
+ return new RestTraverser((Collection) col,restNode.getRestApi());
+ }
+
+ 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);
+ }
+
+ @Override
+ public RestEntity createRestEntity(Map data) {
+ final String uri = (String) data.get("self");
+ if (uri == null || uri.isEmpty()) return null;
+ if (uri.contains("/node/")) {
+ return entityCache.addToCache(new RestNode(data, this));
+ }
+ if (uri.contains("/relationship/")) {
+ return new RestRelationship(data, this);
+ }
+ return null;
+ }
+
+ public RequestResult batch(Collection> batchRequestData) {
+ return restRequest.post("batch",batchRequestData);
+ }
+}
diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIIndex.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIIndex.java
new file mode 100644
index 000000000..7b8a98ce7
--- /dev/null
+++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIIndex.java
@@ -0,0 +1,65 @@
+package org.neo4j.rest.graphdb;
+
+import org.neo4j.graphdb.Node;
+import org.neo4j.graphdb.PropertyContainer;
+import org.neo4j.graphdb.Relationship;
+import org.neo4j.graphdb.index.IndexHits;
+import org.neo4j.rest.graphdb.entity.RestNode;
+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 java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author mh
+ * @since 21.09.14
+ */
+public interface RestAPIIndex {
+ RestIndexManager index();
+
+ @SuppressWarnings("unchecked")
+ RestIndex getIndex(String indexName);
+
+ @SuppressWarnings("unchecked")
+ void createIndex(String type, String indexName, Map config);
+
+ @SuppressWarnings("unchecked")
+ IndexHits getIndex(Class entityType, String indexName, String key, Object value);
+ IndexHits queryIndex(Class entityType, String indexName, String key, Object value);
+
+ IndexInfo indexInfo(String indexType);
+
+ void removeFromIndex(RestIndex index, T entity, String key, Object value);
+
+ void removeFromIndex(RestIndex index, T entity, String key);
+
+ void removeFromIndex(RestIndex index, T entity);
+
+ void addToIndex(T entity, RestIndex index, String key, Object value);
+
+ @SuppressWarnings("unchecked")
+ T putIfAbsent(T entity, RestIndex index, String key, Object value);
+
+ RestNode getOrCreateNode(RestIndex index, String key, Object value, Map properties, Collection labels);
+
+ RestRelationship getOrCreateRelationship(RestIndex index, String key, Object value, RestNode start, RestNode end, String type, Map properties);
+
+ @SuppressWarnings("unchecked")
+ RestIndex createIndex(Class type, String indexName, Map config);
+
+ boolean isAutoIndexingEnabled(Class extends PropertyContainer> clazz);
+
+ void setAutoIndexingEnabled(Class extends PropertyContainer> clazz, boolean enabled);
+
+ Set getAutoIndexedProperties(Class forClass);
+
+ void startAutoIndexingProperty(Class forClass, String s);
+
+ void stopAutoIndexingProperty(Class forClass, String s);
+
+ void delete(RestIndex index);
+}
diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIInternal.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIInternal.java
new file mode 100644
index 000000000..0169e8d91
--- /dev/null
+++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestAPIInternal.java
@@ -0,0 +1,30 @@
+package org.neo4j.rest.graphdb;
+
+import org.neo4j.rest.graphdb.converter.RestEntityExtractor;
+import org.neo4j.rest.graphdb.entity.RestEntity;
+import org.neo4j.rest.graphdb.entity.RestNode;
+
+import java.util.Map;
+
+/**
+ * @author mh
+ * @since 21.09.14
+ */
+public interface RestAPIInternal {
+ RestNode getNodeById(long id, RestAPI.Load force);
+
+ boolean hasToUpdate(long lastUpdate);
+
+ String getBaseUri();
+
+ RestEntityExtractor getEntityExtractor();
+
+ // todo add to cache or update data in cache
+ RestEntity createRestEntity(Map data);
+
+ public enum Load {
+ FromCache,
+ FromServer,
+ ForceFromServer
+ }
+}
diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestGraphDatabase.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestGraphDatabase.java
new file mode 100644
index 000000000..cb81a4db2
--- /dev/null
+++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestGraphDatabase.java
@@ -0,0 +1,163 @@
+/**
+ * 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.RestCypherQueryEngine;
+import org.neo4j.rest.graphdb.transaction.NullTransactionManager;
+import org.neo4j.rest.graphdb.traversal.RestTraversal;
+import org.neo4j.rest.graphdb.traversal.RestTraversalDescription;
+import org.neo4j.rest.graphdb.util.ResourceIterableWrapper;
+
+import javax.transaction.TransactionManager;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+
+
+public class RestGraphDatabase extends AbstractRemoteDatabase {
+ private RestAPI restAPI;
+ private final RestCypherQueryEngine cypherQueryEngine;
+
+
+ public RestGraphDatabase( RestAPI api){
+ this.restAPI = api;
+ cypherQueryEngine = new RestCypherQueryEngine(restAPI);
+ }
+
+ public RestGraphDatabase( String uri ) {
+ this( new RestAPIImpl( uri ));
+ }
+
+ public RestGraphDatabase( 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 cypherQueryEngine.query("match (n) return n", null).to(Node.class);
+ }
+
+ @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 TransactionManager getTxManager() {
+ return new NullTransactionManager();
+ }
+
+ @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() {
+ restAPI.close();
+ }
+
+ @Override
+ public Node createNode(Label... labels) {
+ LinkedHashSet labelNames = new LinkedHashSet<>(labels.length);
+ for (int i = 0; i < labels.length; i++) labelNames.add(labels[i].name());
+ return restAPI.createNode(null,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 RestTraversal.description();
+ }
+
+ @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/RestRequest.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestRequest.java
new file mode 100644
index 000000000..a478f63db
--- /dev/null
+++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestRequest.java
@@ -0,0 +1,42 @@
+/**
+ * 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 java.util.Map;
+
+public interface RestRequest {
+
+ RequestResult get(String path);
+
+ RequestResult get(String path, Object data);
+
+ RequestResult delete(String path);
+
+ RequestResult post(String path, Object data);
+
+ RequestResult put(String path, Object data);
+
+ RestRequest with(String uri);
+
+ String getUri();
+
+ Map, ?> toMap( RequestResult requestResult);
+
+}
\ No newline at end of file
diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestResultException.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestResultException.java
new file mode 100644
index 000000000..1738fb23c
--- /dev/null
+++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestResultException.java
@@ -0,0 +1,80 @@
+/**
+ * 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 java.util.List;
+import java.util.Map;
+
+public class RestResultException extends RuntimeException {
+ public RestResultException(Object result, String...messages) {
+ super(format(toMap(result),messages));
+ }
+
+ public RestResultException(String...messages) {
+ super(format(messages));
+ }
+
+ private static String format(String[] messages) {
+ StringBuilder sb = new StringBuilder();
+ if (messages==null || messages.length==0) return "";
+ for (String s : messages) {
+ sb.append(s).append("\n");
+ }
+ return sb.toString();
+ }
+
+ private static String format(Map, ?> result, String...messages) {
+ if (result==null && (messages==null || messages.length==0)) return "Unknown Exception";
+ StringBuilder sb = new StringBuilder();
+ sb.append(format(messages));
+ sb.append(result.get("message")).append(" at\n");
+ sb.append(result.get("exception")).append("\n");
+ List stacktrace = (List) result.get("stacktrace");
+ if (stacktrace != null) {
+ for (String line : stacktrace) {
+ sb.append(" ").append(line).append("\n");
+ }
+ }
+ if (result.containsKey("body")) sb.append(format(toMap(result.get("body"))));
+ return sb.toString();
+ }
+
+ public static boolean isExceptionResult(Object result) {
+ final Map map = toMap(result);
+ return map!=null && (hasErrorStatus(map) || map.containsKey("exception") || map.containsKey("message") || isExceptionResult(map.get("body")));
+ }
+
+ private static boolean hasErrorStatus(Map map) {
+ Object status = map.get("status");
+ return status != null && !status.toString().startsWith("2");
+ }
+
+ private static Map