From e79071b37c2e0a6797429c30e78a4181911b308a Mon Sep 17 00:00:00 2001 From: Michael Hunger Date: Mon, 28 Mar 2011 12:11:09 +0200 Subject: [PATCH] added rest-wrapper --- pom.xml | 1 + spring-data-neo4j-rest/pom.xml | 133 ++++++++++++++ .../java/org/neo4j/kernel/RestConfig.java | 133 ++++++++++++++ .../rest/graphdb/GraphDatabaseFactory.java | 39 ++++ .../org/neo4j/rest/graphdb/JsonHelper.java | 58 ++++++ .../org/neo4j/rest/graphdb/PropertiesMap.java | 139 ++++++++++++++ .../org/neo4j/rest/graphdb/RestEntity.java | 169 ++++++++++++++++++ .../neo4j/rest/graphdb/RestGraphDatabase.java | 140 +++++++++++++++ .../java/org/neo4j/rest/graphdb/RestNode.java | 139 ++++++++++++++ .../neo4j/rest/graphdb/RestPathParser.java | 56 ++++++ .../neo4j/rest/graphdb/RestRelationship.java | 61 +++++++ .../org/neo4j/rest/graphdb/RestRequest.java | 122 +++++++++++++ .../org/neo4j/rest/graphdb/RestTraversal.java | 155 ++++++++++++++++ .../graphdb/RestTraversalDescription.java | 19 ++ .../org/neo4j/rest/graphdb/RestTraverser.java | 54 ++++++ .../org/neo4j/rest/graphdb/SimplePath.java | 86 +++++++++ .../neo4j/rest/graphdb/index/RestIndex.java | 137 ++++++++++++++ .../rest/graphdb/index/RestIndexManager.java | 77 ++++++++ .../rest/graphdb/index/RestNodeIndex.java | 34 ++++ .../graphdb/index/RestRelationshipIndex.java | 48 +++++ .../rest/graphdb/util/ArrayConverter.java | 62 +++++++ .../graphdb/IsRelationshipToNodeMatcher.java | 36 ++++ .../neo4j/rest/graphdb/LocalTestServer.java | 96 ++++++++++ .../rest/graphdb/Neo4jDatabaseCleaner.java | 67 +++++++ .../neo4j/rest/graphdb/RestEntityTest.java | 70 ++++++++ .../neo4j/rest/graphdb/RestGraphDbTest.java | 56 ++++++ .../org/neo4j/rest/graphdb/RestIndexTest.java | 93 ++++++++++ .../org/neo4j/rest/graphdb/RestTestBase.java | 71 ++++++++ .../graphdb/RestTraversalExecutionTest.java | 21 +++ .../neo4j/rest/graphdb/RestTraversalTest.java | 111 ++++++++++++ .../java/org/neo4j/rest/graphdb/Type.java | 11 ++ .../src/test/resources/log4j.properties | 26 +++ .../src/test/resources/test-db.properties | 1 + 33 files changed, 2521 insertions(+) create mode 100644 spring-data-neo4j-rest/pom.xml create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/kernel/RestConfig.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/GraphDatabaseFactory.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/JsonHelper.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/PropertiesMap.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestEntity.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestGraphDatabase.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestNode.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestPathParser.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestRelationship.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestRequest.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestTraversal.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestTraversalDescription.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestTraverser.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/SimplePath.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestIndex.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestIndexManager.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestNodeIndex.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestRelationshipIndex.java create mode 100644 spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/ArrayConverter.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/IsRelationshipToNodeMatcher.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/LocalTestServer.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/Neo4jDatabaseCleaner.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestEntityTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestGraphDbTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestIndexTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTestBase.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTraversalExecutionTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTraversalTest.java create mode 100644 spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/Type.java create mode 100644 spring-data-neo4j-rest/src/test/resources/log4j.properties create mode 100644 spring-data-neo4j-rest/src/test/resources/test-db.properties diff --git a/pom.xml b/pom.xml index 445e78622..b21a6853e 100644 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,7 @@ spring-data-graph-core spring-data-neo4j spring-data-neo4j-roo + spring-data-neo4j-rest diff --git a/spring-data-neo4j-rest/pom.xml b/spring-data-neo4j-rest/pom.xml new file mode 100644 index 000000000..7efd5b256 --- /dev/null +++ b/spring-data-neo4j-rest/pom.xml @@ -0,0 +1,133 @@ + + 4.0.0 + + org.springframework.data + spring-data-graph-parent + 1.0.0.BUILD-SNAPSHOT + ../spring-data-graph-parent/pom.xml + + org.neo4j + spring-data-neo4j-rest + jar + 1.0.0.BUILD-SNAPSHOT + Spring Data Graph REST Wrapper + + UTF-8 + 1.5.10 + 1.4 + 1.3.M05 + + + + + neo4j + http://m2.neo4j.org + + + Sun + http://download.java.net/maven/2 + + + + + junit + junit + test + + + org.neo4j + neo4j + + + org.neo4j.app + neo4j-server + ${neo4j.version} + test + + + com.tinkerpop + gremlin + + + org.neo4j + neo4j + + + org.mortbay.jetty + jetty + + + com.sun.jersey + jersey-server + + + org.codehaus.jackson + jackson-jaxrs + + + org.codehaus.jackson + jackson-mapper-asl + + + de.huxhorn.lilith + de.huxhorn.lilith.3rdparty.rrd4j + + + com.sun.jersey.contribs + jersey-multipart + + + org.apache.felix + org.apache.felix.main + + + org.apache.felix + org.apache.felix.fileinstall + + + org.neo4j + neo4j-shell + + + + + org.mortbay.jetty + jetty + 6.1.25 + test + + + + org.codehaus.jackson + jackson-jaxrs + 1.6.1 + + + org.codehaus.jackson + jackson-mapper-asl + 1.6.1 + + + com.sun.jersey + jersey-server + ${jersey.version} + + + com.sun.jersey + jersey-client + ${jersey.version} + + + + + + maven-compiler-plugin + + 1.6 + 1.6 + + + + + diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/kernel/RestConfig.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/kernel/RestConfig.java new file mode 100644 index 000000000..87a15ac0b --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/kernel/RestConfig.java @@ -0,0 +1,133 @@ +package org.neo4j.kernel; + +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.kernel.Config; +import org.neo4j.kernel.IdGeneratorFactory; +import org.neo4j.kernel.impl.core.*; +import org.neo4j.kernel.impl.nioneo.store.FileSystemAbstraction; +import org.neo4j.kernel.impl.nioneo.store.StoreId; +import org.neo4j.kernel.impl.transaction.LockManager; +import org.neo4j.kernel.impl.transaction.TxModule; +import org.neo4j.kernel.impl.transaction.xaframework.LogBufferFactory; +import org.neo4j.kernel.impl.transaction.xaframework.TxIdGenerator; +import org.neo4j.rest.graphdb.RestGraphDatabase; + +import javax.transaction.*; +import javax.transaction.xa.XAResource; +import java.util.Collections; +import java.util.Map; + +/** +* @author mh +* @since 23.02.11 +*/ +public class RestConfig extends Config { + public RestConfig(GraphDatabaseService graphDb, String storeDir, StoreId storeId, + Map inputParams, KernelPanicEventGenerator kpe, + TxModule txModule, LockManager lockManager, LockReleaser lockReleaser, + IdGeneratorFactory idGeneratorFactory, + TxEventSyncHookFactory txSyncHookFactory, RelationshipTypeCreator relTypeCreator, + TxIdGenerator txIdGenerator, LastCommittedTxIdSetter lastCommittedTxIdSetter, + FileSystemAbstraction fileSystem, LogBufferFactory logBufferFactory) { + super(graphDb, storeDir, storeId, + inputParams, kpe, txModule, lockManager, lockReleaser, idGeneratorFactory, txSyncHookFactory, relTypeCreator, txIdGenerator, lastCommittedTxIdSetter, fileSystem, logBufferFactory); + } + + public RestConfig(RestGraphDatabase restGraphDatabase) { + super(restGraphDatabase, restGraphDatabase.getStoreDir(), null, + Collections.emptyMap(),null, + new TxModule(true,null){ + @Override + public TransactionManager getTxManager() { + return new NullTransactionManager(); + } + }, + null,null, + null,null,null,null,null, + null,null); + } + + private static class NullTransactionManager implements TransactionManager { + private static final Transaction TRANSACTION = new Transaction() { + @Override + public void commit() throws HeuristicMixedException, HeuristicRollbackException, RollbackException, SecurityException, SystemException { + + } + + @Override + public boolean delistResource(XAResource xaResource, int i) throws IllegalStateException, SystemException { + return false; + } + + @Override + public boolean enlistResource(XAResource xaResource) throws IllegalStateException, RollbackException, SystemException { + return false; + } + + @Override + public int getStatus() throws SystemException { + return Status.STATUS_NO_TRANSACTION; + } + + @Override + public void registerSynchronization(Synchronization synchronization) throws IllegalStateException, RollbackException, SystemException { + + } + + @Override + public void rollback() throws IllegalStateException, SystemException { + + } + + @Override + public void setRollbackOnly() throws IllegalStateException, SystemException { + + } + }; + + @Override + public void begin() throws NotSupportedException, SystemException { + + } + + @Override + public void commit() throws HeuristicMixedException, HeuristicRollbackException, IllegalStateException, RollbackException, SecurityException, SystemException { + + } + + @Override + public int getStatus() throws SystemException { + return 0; + } + + @Override + public Transaction getTransaction() throws SystemException { + return TRANSACTION; + } + + @Override + public void resume(Transaction transaction) throws IllegalStateException, InvalidTransactionException, SystemException { + + } + + @Override + public void rollback() throws IllegalStateException, SecurityException, SystemException { + + } + + @Override + public void setRollbackOnly() throws IllegalStateException, SystemException { + + } + + @Override + public void setTransactionTimeout(int i) throws SystemException { + + } + + @Override + public Transaction suspend() throws SystemException { + return TRANSACTION; + } + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/GraphDatabaseFactory.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/GraphDatabaseFactory.java new file mode 100644 index 000000000..497c61737 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/GraphDatabaseFactory.java @@ -0,0 +1,39 @@ +package org.neo4j.rest.graphdb; + +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.kernel.EmbeddedGraphDatabase; + +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; + +/** + * @author mh + * @since 25.01.11 + */ +public class GraphDatabaseFactory { + public static GraphDatabaseService databaseFor(String url) { + return databaseFor( url, null,null ); + } + + public static GraphDatabaseService databaseFor(String url, String username, String password) { + if (url.startsWith( "http://" ) || url.startsWith( "https://" )) { + return new RestGraphDatabase( toURI( url ), username,password ); + } + String path=url; + if (url.startsWith( "file:" )) { + path = toURI( url ).getPath(); + } + File file = new File( path ); + if (!file.isDirectory()) file=file.getParentFile(); + return new EmbeddedGraphDatabase( file.getAbsolutePath() ); + } + + private static URI toURI( String uri ) { + try { + return new URI(uri); + } catch ( URISyntaxException e ) { + throw new RuntimeException( "Error using URI "+uri, e); + } + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/JsonHelper.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/JsonHelper.java new file mode 100644 index 000000000..de6a2a764 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/JsonHelper.java @@ -0,0 +1,58 @@ +package org.neo4j.rest.graphdb; + +/** + * @author mh + * @since 13.12.10 + */ + +import org.codehaus.jackson.JsonGenerator; +import org.codehaus.jackson.map.ObjectMapper; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class JsonHelper { + + static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @SuppressWarnings("unchecked") + public static Map jsonToMap( String json ) { + return (Map) readJson( json ); + } + + @SuppressWarnings("unchecked") + public static List> jsonToListOfRelationshipRepresentations( String json ) { + return (List>) readJson( json ); + } + + private static Object readJson( String json ) { + ObjectMapper mapper = new ObjectMapper(); + try { + return mapper.readValue( json, Object.class ); + } catch ( IOException e ) { + throw new RuntimeException( e ); + } + } + + public static Object jsonToSingleValue( String json ) { + Object jsonObject = readJson( json ); + return jsonObject instanceof Collection ? jsonObject : + PropertiesMap.assertSupportedPropertyValue( jsonObject ); + } + + public static String createJsonFrom( Object data ) { + try { + StringWriter writer = new StringWriter(); + JsonGenerator generator = OBJECT_MAPPER.getJsonFactory() + .createJsonGenerator( writer ).useDefaultPrettyPrinter(); + OBJECT_MAPPER.writeValue( generator, data ); + writer.close(); + return writer.getBuffer().toString(); + } catch ( IOException e ) { + throw new RuntimeException( e ); + } + } +} \ No newline at end of file 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..71e9cf822 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/PropertiesMap.java @@ -0,0 +1,139 @@ +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" ); + } + + if ( value instanceof String ) { + } else if ( value instanceof Number ) { + } else if ( value instanceof Boolean ) { + } else { + throw new RuntimeException( "Unsupported value type " + value.getClass() + "." + + " Supported value types are all java primitives (byte, char, short, int, " + + "long, float, double) and String, as well as arrays of all those types" ); + } + return value; + } + + 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/RestEntity.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestEntity.java new file mode 100644 index 000000000..2b9c201a9 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestEntity.java @@ -0,0 +1,169 @@ +package org.neo4j.rest.graphdb; + +import com.sun.jersey.api.client.ClientResponse; +import org.neo4j.graphdb.NotFoundException; +import org.neo4j.graphdb.PropertyContainer; +import org.neo4j.helpers.collection.IterableWrapper; +import org.neo4j.rest.graphdb.util.ArrayConverter; + +import javax.ws.rs.core.Response.Status; +import java.net.URI; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +public class RestEntity implements PropertyContainer { + private Map structuralData; + private Map propertyData; + private long lastTimeFetchedPropertyData; + private RestGraphDatabase graphDatabase; + protected RestRequest restRequest; + private final ArrayConverter arrayConverter=new ArrayConverter(); + + public RestEntity( URI uri, RestGraphDatabase graphDatabase ) { + this( uri.toString(), graphDatabase ); + } + + public RestEntity( String uri, RestGraphDatabase graphDatabase ) { + this.restRequest = graphDatabase.getRestRequest().with( uri ); + this.graphDatabase = graphDatabase; + } + + public RestEntity( Map data, RestGraphDatabase graphDatabase ) { + this.structuralData = data; + this.graphDatabase = graphDatabase; + this.propertyData = (Map) data.get( "data" ); + this.lastTimeFetchedPropertyData = System.currentTimeMillis(); + String uri = (String) data.get( "self" ); + this.restRequest = graphDatabase.getRestRequest().with( uri ); + } + + public String getUri() { + return this.restRequest.getUri().toString(); + } + + Map getStructuralData() { + if ( this.structuralData == null ) { + this.structuralData = restRequest.toMap( restRequest.get( "" ) ); + } + return this.structuralData; + } + + Map getPropertyData() { + if ( this.propertyData == null || timeElapsed( this.lastTimeFetchedPropertyData, graphDatabase.getPropertyRefetchTimeInMillis() ) ) { + ClientResponse response = restRequest.get( "properties" ); + boolean ok = restRequest.statusIs( response, Status.OK ); + if ( ok ) { + this.propertyData = (Map) restRequest.toMap( response ); + } else { + this.propertyData = Collections.emptyMap(); + } + this.lastTimeFetchedPropertyData = System.currentTimeMillis(); + } + return this.propertyData; + } + + private boolean timeElapsed( long since, long isItGreaterThanThis ) { + return System.currentTimeMillis() - since > isItGreaterThanThis; + } + + public Object getProperty( String key ) { + Object value = getPropertyValue( key ); + if ( value == null ) { + throw new NotFoundException( "'" + key + "' on " + this ); + } + return value; + } + + private Object getPropertyValue( String key ) { + Map properties = getPropertyData(); + Object value = properties.get( key ); + if ( value == null) return null; + if ( value instanceof Collection ) { + Collection col= (Collection) value; + if (col.isEmpty()) return new String[0]; // todo concrete value type ? + Object result = arrayConverter.toArray( col ); + if (result == null) throw new IllegalStateException( "Could not determine type of property "+key ); + properties.put(key,result); + return result; + + } + return PropertiesMap.assertSupportedPropertyValue( value ); + } + + public Object getProperty( String key, Object defaultValue ) { + Object value = getPropertyValue( key ); + return value != null ? value : defaultValue; + } + + @SuppressWarnings("unchecked") + public Iterable getPropertyKeys() { + return new IterableWrapper( getPropertyData().keySet() ) { + @Override + protected String underlyingObjectToObject( Object key ) { + return key.toString(); + } + }; + } + + @SuppressWarnings("unchecked") + public Iterable getPropertyValues() { + return (Iterable) getPropertyData().values(); + } + + public boolean hasProperty( String key ) { + return getPropertyData().containsKey( key ); + } + + public Object removeProperty( String key ) { + Object value = getProperty( key, null ); + restRequest.delete( "properties/" + key ); + invalidatePropertyData(); + return value; + } + + public void setProperty( String key, Object value ) { + restRequest.put( "properties/" + key, JsonHelper.createJsonFrom( value ) ); + invalidatePropertyData(); + } + + private void invalidatePropertyData() { + this.propertyData = null; + } + + static long getEntityId( String uri ) { + return Long.parseLong( uri.substring( uri.lastIndexOf( '/' ) + 1 ) ); + } + + public long getId() { + return getEntityId( getUri() ); + } + + public void delete() { + restRequest.delete( "" ); + } + + @Override + public int hashCode() { + return (int) getId(); + } + + @Override + public boolean equals( Object o ) { + if (o == null) return false; + return getClass().equals( o.getClass() ) && getId() == ( (RestEntity) o ).getId(); + } + + public RestGraphDatabase getGraphDatabase() { + return graphDatabase; + } + + public RestRequest getRestRequest() { + return restRequest; + } + + @Override + public String toString() { + return getUri(); + } +} 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..c360f9a8b --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestGraphDatabase.java @@ -0,0 +1,140 @@ +package org.neo4j.rest.graphdb; + +import com.sun.jersey.api.NotFoundException; +import com.sun.jersey.api.client.ClientResponse; +import org.neo4j.graphdb.*; +import org.neo4j.graphdb.event.KernelEventHandler; +import org.neo4j.graphdb.event.TransactionEventHandler; +import org.neo4j.graphdb.index.IndexManager; +import org.neo4j.kernel.AbstractGraphDatabase; +import org.neo4j.kernel.Config; +import org.neo4j.kernel.RestConfig; +import org.neo4j.rest.graphdb.index.RestIndexManager; + +import javax.ws.rs.core.Response.Status; +import java.io.Serializable; +import java.net.URI; +import java.util.Map; + +public class RestGraphDatabase extends AbstractGraphDatabase { + private RestRequest restRequest; + private long propertyRefetchTimeInMillis = 1000; + + + public RestGraphDatabase( URI uri ) { + restRequest = new RestRequest( uri ); + } + + public RestGraphDatabase( URI uri, String user, String password ) { + restRequest = new RestRequest( uri, user, password ); + } + + public Transaction beginTx() { + return new Transaction() { + public void success() { + } + + public void finish() { + + } + + public void failure() { + } + }; + } + + 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(); + } + + public IndexManager index() { + return new RestIndexManager( restRequest, this ); + } + + public Node createNode() { + ClientResponse response = restRequest.post( "node", null ); + if ( restRequest.statusOtherThan( response, Status.CREATED ) ) { + throw new RuntimeException( "" + response.getStatus() ); + } + return new RestNode( response.getLocation(), this ); + } + + public boolean enableRemoteShell() { + throw new UnsupportedOperationException(); + } + + public boolean enableRemoteShell( Map config ) { + throw new UnsupportedOperationException(); + } + + public Iterable getAllNodes() { + throw new UnsupportedOperationException(); + } + + public Node getNodeById( long id ) { + ClientResponse response = restRequest.get( "node/" + id ); + if ( restRequest.statusIs( response, Status.NOT_FOUND ) ) { + throw new NotFoundException( "" + id ); + } + return new RestNode( restRequest.toMap( response ), this ); + } + + public Node getReferenceNode() { + Map map = restRequest.toMap( restRequest.get( "" ) ); + return new RestNode( (String) map.get( "reference_node" ), this ); + } + + public Relationship getRelationshipById( long id ) { + ClientResponse response = restRequest.get( "relationship/" + id ); + if ( restRequest.statusIs( response, Status.NOT_FOUND ) ) { + throw new NotFoundException( "" + id ); + } + return new RestRelationship( restRequest.toMap( response ), this ); + } + + public Iterable getRelationshipTypes() { + throw new UnsupportedOperationException(); + } + + public void shutdown() { + } + + public RestRequest getRestRequest() { + return restRequest; + } + + public long getPropertyRefetchTimeInMillis() { + return propertyRefetchTimeInMillis; + } + @Override + public String getStoreDir() { + return restRequest.getUri().toString(); + } + + @Override + public Config getConfig() { + return new RestConfig(this); + } + + @Override + public T getManagementBean(Class type) { + return null; + } + + @Override + public boolean isReadOnly() { + return false; + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestNode.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestNode.java new file mode 100644 index 000000000..27bdb2a60 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestNode.java @@ -0,0 +1,139 @@ +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 org.neo4j.graphdb.*; +import org.neo4j.graphdb.Traverser.Order; +import org.neo4j.helpers.collection.IterableWrapper; +import org.neo4j.helpers.collection.IteratorUtil; +import org.neo4j.helpers.collection.MapUtil; + +import javax.ws.rs.core.Response.Status; +import java.net.URI; +import java.util.Collection; +import java.util.Map; + +public class RestNode extends RestEntity implements Node { + public RestNode( URI uri, RestGraphDatabase graphDatabase ) { + super( uri, graphDatabase ); + } + + public RestNode( String uri, RestGraphDatabase graphDatabase ) { + super( uri, graphDatabase ); + } + + public RestNode( Map data, RestGraphDatabase graphDatabase ) { + super( data, graphDatabase ); + } + + public Relationship createRelationshipTo( Node toNode, RelationshipType type ) { + Map data = MapUtil.map( "to", ( (RestNode) toNode ).getUri(), + "type", type.name() ); + + ClientResponse response = restRequest.post( "relationships", JsonHelper.createJsonFrom( data ) ); + if ( restRequest.statusOtherThan( response, Status.CREATED ) ) { + throw new RuntimeException( "" + response.getStatus() ); + } + return new RestRelationship( response.getLocation(), getGraphDatabase() ); + } + + public Iterable getRelationships() { + return wrapRelationships( restRequest.get( "relationships/all" ) ); + } + + @SuppressWarnings("unchecked") + private Iterable wrapRelationships( ClientResponse response ) { + return new IterableWrapper( + (Collection) restRequest.toEntity( response ) ) { + @Override + protected Relationship underlyingObjectToObject( Object data ) { + return new RestRelationship( (Map) data, getGraphDatabase() ); + } + }; + } + + public Iterable getRelationships( RelationshipType... types ) { + String path = getStructuralData().get( "all_relationships" ) + "/"; + int counter = 0; + for ( RelationshipType type : types ) { + if ( counter++ > 0 ) { + path += "&"; + } + path += type.name(); + } + return wrapRelationships( restRequest.get( path ) ); + } + + + enum RestDirection { + INCOMING( Direction.INCOMING, "incoming", "in" ), + OUTGOING( Direction.OUTGOING, "outgoing", "out" ), + BOTH( Direction.BOTH, "all", "all" ); + + public final Direction direction; + public final String dataName; + public final String pathName; + + RestDirection( Direction direction, String dataName, String pathName ) { + this.direction = direction; + this.dataName = dataName; + this.pathName = pathName; + } + + static RestDirection from( Direction direction ) { + for ( RestDirection restDirection : values() ) { + if ( restDirection.direction == direction ) return restDirection; + } + throw new RuntimeException( "No Rest-Direction for " + direction ); + } + } + + public Iterable getRelationships( Direction direction ) { + return wrapRelationships( restRequest.get( "relationships/" + RestDirection.from( direction ).pathName ) ); + } + + public Iterable getRelationships( RelationshipType type, + Direction direction ) { + String relationshipsKey = RestDirection.from( direction ).dataName + "_relationships"; + Object relationship = getStructuralData().get( relationshipsKey ); + return wrapRelationships( restRequest.get( relationship + "/" + type.name() ) ); + } + + public Relationship getSingleRelationship( RelationshipType type, + Direction direction ) { + return IteratorUtil.singleOrNull( getRelationships( type, direction ) ); + } + + public boolean hasRelationship() { + return getRelationships().iterator().hasNext(); + } + + public boolean hasRelationship( RelationshipType... types ) { + return getRelationships( types ).iterator().hasNext(); + } + + public boolean hasRelationship( Direction direction ) { + return getRelationships( direction ).iterator().hasNext(); + } + + public boolean hasRelationship( RelationshipType type, Direction direction ) { + return getRelationships( type, direction ).iterator().hasNext(); + } + + public Traverser traverse( Order order, StopEvaluator stopEvaluator, + ReturnableEvaluator returnableEvaluator, Object... rels ) { + throw new UnsupportedOperationException(); + } + + public Traverser traverse( Order order, StopEvaluator stopEvaluator, + ReturnableEvaluator returnableEvaluator, RelationshipType type, Direction direction ) { + throw new UnsupportedOperationException(); + } + + public Traverser traverse( Order order, StopEvaluator stopEvaluator, + ReturnableEvaluator returnableEvaluator, RelationshipType type, Direction direction, + RelationshipType secondType, Direction secondDirection ) { + throw new UnsupportedOperationException(); + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestPathParser.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestPathParser.java new file mode 100644 index 000000000..4a8c70c35 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestPathParser.java @@ -0,0 +1,56 @@ +package org.neo4j.rest.graphdb; + +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Path; +import org.neo4j.graphdb.Relationship; +import org.neo4j.helpers.collection.IterableWrapper; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * @author Michael Hunger + * @since 03.02.11 + */ +public class RestPathParser { + public static Path parse(Map path, final RestGraphDatabase restGraphDatabase) { + final Collection> nodesData = (Collection>) path.get("nodes"); + final Collection> relationshipsData = (Collection>) path.get("relationships"); + final Map lastRelationshipData = lastElement(relationshipsData); + final Map startData = (Map) path.get("start"); + final Map endData = (Map) path.get("end"); + final Integer length = (Integer) path.get("length"); + + return new SimplePath( + new RestNode(startData,restGraphDatabase), + new RestNode(endData,restGraphDatabase), + new RestRelationship(lastRelationshipData,restGraphDatabase), + length, + new IterableWrapper>(nodesData) { + @Override + protected Node underlyingObjectToObject(Map data) { + return new RestNode(data,restGraphDatabase); + } + }, + new IterableWrapper>(relationshipsData) { + @Override + protected Relationship underlyingObjectToObject(Map data) { + return new RestRelationship(data,restGraphDatabase); + } + }); + } + + private static Map lastElement(Collection> collection) { + if (collection.isEmpty()) return null; + if (collection instanceof List) { + List> list = (List>) collection; + return list.get(list.size()-1); + } + Map result = null; + for (Map value : collection) { + result=value; + } + return result; + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestRelationship.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestRelationship.java new file mode 100644 index 000000000..1ec933624 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestRelationship.java @@ -0,0 +1,61 @@ +package org.neo4j.rest.graphdb; + +import org.neo4j.graphdb.*; + +import java.net.URI; +import java.util.Map; + +public class RestRelationship extends RestEntity implements Relationship { + + RestRelationship( URI uri, RestGraphDatabase graphDatabaseService ) { + super( uri, graphDatabaseService ); + } + + RestRelationship( String uri, RestGraphDatabase graphDatabase ) { + super( uri, graphDatabase ); + } + + public RestRelationship( Map data, RestGraphDatabase graphDatabase ) { + super( data, graphDatabase ); + } + + public Node getEndNode() { + return node( (String) getStructuralData().get( "end" ) ); + } + + public Node[] getNodes() { + return new Node[]{ + node( (String) getStructuralData().get( "start" ) ), + node( (String) getStructuralData().get( "end" ) ) + }; + } + + public Node getOtherNode( Node node ) { + long nodeId = node.getId(); + String startNodeUri = (String) getStructuralData().get( "start" ); + String endNodeUri = (String) getStructuralData().get( "end" ); + if ( getEntityId( startNodeUri ) == nodeId ) { + return node( endNodeUri ); + } else if ( getEntityId( endNodeUri ) == nodeId ) { + return node( startNodeUri ); + } else { + throw new NotFoundException( node + " isn't one of start/end for " + this ); + } + } + + private RestNode node( String uri ) { + return new RestNode( uri, getGraphDatabase() ); + } + + public Node getStartNode() { + return node( (String) getStructuralData().get( "start" ) ); + } + + public RelationshipType getType() { + return DynamicRelationshipType.withName( (String) getStructuralData().get( "type" ) ); + } + + public boolean isType( RelationshipType type ) { + return type.name().equals( getStructuralData().get( "type" ) ); + } +} 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..3ebbc5ad3 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestRequest.java @@ -0,0 +1,122 @@ +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 javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.util.Map; + +public class RestRequest { + private final URI baseUri; + private final Client client; + + public RestRequest( URI baseUri ) { + this( baseUri, null, null ); + } + + public RestRequest( URI baseUri, String username, String password ) { + this.baseUri = uriWithoutSlash( baseUri ); + client = Client.create(); + if ( username != null ) client.addFilter( new HTTPBasicAuthFilter( username, password ) ); + + } + + private RestRequest( URI uri, Client client ) { + this.baseUri = uriWithoutSlash( uri ); + this.client = client; + } + + private URI uriWithoutSlash( URI uri ) { + String uriString = uri.toString(); + return uriString.endsWith( "/" ) ? uri( uriString.substring( 0, uriString.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 ) ) ); + return resource.accept( MediaType.APPLICATION_JSON_TYPE ); + } + + private String pathOrAbsolute( String path ) { + if ( path.startsWith( "http://" ) ) return path; + return baseUri + "/" + path; + } + + public ClientResponse get( String path ) { + return builder( path ).get( ClientResponse.class ); + } + + public ClientResponse delete( String path ) { + return builder( path ).delete( ClientResponse.class ); + } + + public ClientResponse post( String path, String data ) { + Builder builder = builder( path ); + if ( data != null ) { + builder = builder.entity( data, MediaType.APPLICATION_JSON_TYPE ); + } + return builder.post( ClientResponse.class ); + } + + public ClientResponse put( String path, String data ) { + Builder builder = builder( path ); + if ( data != null ) { + builder = builder.entity( data, MediaType.APPLICATION_JSON_TYPE ); + } + return builder.put( ClientResponse.class ); + } + + + public Object toEntity( ClientResponse response ) { + return JsonHelper.jsonToSingleValue( entityString( response ) ); + } + + public Map toMap( ClientResponse response ) { + return JsonHelper.jsonToMap( entityString( response ) ); + } + + private String entityString( ClientResponse response ) { + return response.getEntity( String.class ); + } + + public boolean statusIs( ClientResponse response, Response.StatusType status ) { + return response.getStatus() == status.getStatusCode(); + } + + public boolean statusOtherThan( ClientResponse response, Response.StatusType status ) { + return !statusIs( response, status ); + } + + public RestRequest with( String uri ) { + return new RestRequest( uri( uri ), client ); + } + + private URI uri( String uri ) { + try { + return new URI( uri ); + } catch ( URISyntaxException e ) { + throw new RuntimeException( e ); + } + } + + public URI getUri() { + return baseUri; + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestTraversal.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestTraversal.java new file mode 100644 index 000000000..565c7eec0 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestTraversal.java @@ -0,0 +1,155 @@ +package org.neo4j.rest.graphdb; + +import com.sun.jersey.api.client.ClientResponse; +import org.neo4j.graphdb.*; +import org.neo4j.graphdb.traversal.*; +import org.neo4j.graphdb.traversal.Traverser; +import org.neo4j.helpers.Predicate; +import org.neo4j.kernel.Traversal; +import org.neo4j.kernel.Uniqueness; + +import javax.ws.rs.core.Response; +import java.lang.reflect.Field; +import java.util.*; + +/** + * @author Michael Hunger + * @since 02.02.11 + */ +public class RestTraversal implements RestTraversalDescription { + + private static final String FULLPATH = "fullpath"; + private final Map description=new HashMap(); + + @Override + public String toString() { + return description.toString(); + } + + public TraversalDescription uniqueness(UniquenessFactory uniquenessFactory) { + return uniqueness(uniquenessFactory,null); + } + + public TraversalDescription uniqueness(UniquenessFactory uniquenessFactory, Object value) { + String uniqueness = restify(uniquenessFactory); + add("uniqueness",value==null ? uniqueness : toMap("name",uniqueness, "value", value)); + return null; + } + + private String restify(UniquenessFactory uniquenessFactory) { + if (uniquenessFactory instanceof Uniqueness) { + return ((Uniqueness)uniquenessFactory).name().toLowerCase().replace("_"," "); + } + throw new UnsupportedOperationException("Only values of "+Uniqueness.class+" are supported"); + } + + public TraversalDescription prune(PruneEvaluator pruneEvaluator) { + Integer maxDepth= getMaxDepthValueOrNull(pruneEvaluator); + if (maxDepth!=null) { + return maxDepth(maxDepth); + } + throw new UnsupportedOperationException("Only max depth supported"); + } + + private Integer getMaxDepthValueOrNull(PruneEvaluator pruneEvaluator) { + try { + final Field depthField = pruneEvaluator.getClass().getDeclaredField("val$depth"); + depthField.setAccessible(true); + return (Integer) depthField.get(pruneEvaluator); + } catch (Exception e) { + return null; + } + } + + public TraversalDescription filter(Predicate pathPredicate) { + if (pathPredicate == Traversal.returnAll()) return add("return filter",toMap("language","builtin", "name","all")); + if (pathPredicate == Traversal.returnAllButStartNode()) return add("return filter",toMap("language","builtin", "name","all but start node")); + throw new UnsupportedOperationException("Only builtin paths supported"); + } + + public TraversalDescription evaluator(Evaluator evaluator) { + return null; + } + + public TraversalDescription prune(ScriptLanguage language, String code) { + return add("prune evaluator",toMap("language",language.name().toLowerCase(),"body",code )); + } + + public TraversalDescription filter(ScriptLanguage language, String code) { + return add("return filter",toMap("language",language.name().toLowerCase(),"body",code )); + } + + public TraversalDescription maxDepth(int depth) { + return add("max depth",depth); + } + + public TraversalDescription order(BranchOrderingPolicy branchOrderingPolicy) { + throw new UnsupportedOperationException(); + } + + public TraversalDescription depthFirst() { + return add("order","depth first"); + } + + public TraversalDescription breadthFirst() { + return add("order", "breadth first"); + } + + private RestTraversalDescription add(String key, Object value) { + description.put(key,value); + return this; + } + + public TraversalDescription relationships(RelationshipType relationshipType) { + return relationships(relationshipType, null); + } + + public TraversalDescription relationships(RelationshipType relationshipType, Direction direction) { + if (!description.containsKey("relationships")) { + description.put("relationships",new HashSet>()); + } + Set> relationships= (Set>) description.get("relationships"); + relationships.add(toMap("type", relationshipType, "direction", directionString(direction))); + return this; + } + + private Map toMap(Object...params) { + if (params.length % 2 != 0) throw new IllegalArgumentException("toMap needs an even number of arguments, but was "+Arrays.toString(params)); + + Map result = new HashMap(); + for (int i = 0; i < params.length; i+=2) { + if (params[i+1] == null) continue; + result.put(params[i].toString(), params[i + 1].toString()); + } + return result; + } + + private String directionString(Direction direction) { + if (direction==Direction.INCOMING) return "in"; + if (direction==Direction.OUTGOING) return "out"; + return null; + } + + public TraversalDescription expand(RelationshipExpander relationshipExpander) { + return null; + } + + public Traverser traverse(Node node) { + final RestNode restNode = (RestNode) node; + final RestRequest request = restNode.getRestRequest(); + final String traversalJson = JsonHelper.createJsonFrom(description); + final ClientResponse result = request.post("traverse/" + FULLPATH, traversalJson); + if (request.statusOtherThan(result, Response.Status.OK)) throw new RuntimeException(String.format("Error executing traversal: %d %s",result.getStatus(), traversalJson)); + final Object col = request.toEntity(result); + 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.getGraphDatabase()); + } + + public static RestTraversalDescription description() { + return new RestTraversal(); + } + + public Map getPostData() { + return description; + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestTraversalDescription.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestTraversalDescription.java new file mode 100644 index 000000000..e2ced1770 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestTraversalDescription.java @@ -0,0 +1,19 @@ +package org.neo4j.rest.graphdb; + +import org.neo4j.graphdb.traversal.TraversalDescription; + +/** + * @author Michael Hunger + * @since 03.02.11 + */ +public interface RestTraversalDescription extends TraversalDescription { + TraversalDescription prune(ScriptLanguage language, String code); + + TraversalDescription filter(ScriptLanguage language, String code); + + TraversalDescription maxDepth(int depth); + + public enum ScriptLanguage { + JAVASCRIPT; + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestTraverser.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestTraverser.java new file mode 100644 index 000000000..c9344c962 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/RestTraverser.java @@ -0,0 +1,54 @@ +package org.neo4j.rest.graphdb; + +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Path; +import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.traversal.Traverser; +import org.neo4j.helpers.collection.IterableWrapper; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +/** + * @author Michael Hunger + * @since 03.02.11 + */ +public class RestTraverser implements Traverser { + private final Collection paths; + public RestTraverser(Collection col, RestGraphDatabase restGraphDatabase) { + this.paths = parseToPaths(col,restGraphDatabase); + } + + private Collection parseToPaths(Collection col, RestGraphDatabase restGraphDatabase) { + Collection result=new ArrayList(col.size()); + for (Object path : col) { + if (!(path instanceof Map)) throw new RuntimeException("Expected Map for Path representation but got: "+(path!=null ? path.getClass() : null)); + result.add(RestPathParser.parse((Map) path, restGraphDatabase)); + } + return result; + } + + public Iterable nodes() { + return new IterableWrapper(paths) { + @Override + protected Node underlyingObjectToObject(Path path) { + return path.endNode(); + } + }; + } + + public Iterable relationships() { + return new IterableWrapper(paths) { + @Override + protected Relationship underlyingObjectToObject(Path path) { + return path.lastRelationship(); + } + }; + } + + public Iterator iterator() { + return paths.iterator(); + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/SimplePath.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/SimplePath.java new file mode 100644 index 000000000..e52d6f29d --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/SimplePath.java @@ -0,0 +1,86 @@ +package org.neo4j.rest.graphdb; + +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Path; +import org.neo4j.graphdb.PropertyContainer; +import org.neo4j.graphdb.Relationship; + +import java.util.Iterator; + +/** + * @author Michael Hunger + * @since 03.02.11 + */ +public class SimplePath implements Path { + Iterable relationships; + Iterable nodes; + private final Relationship lastRelationship; + private int length; + private Node startNode; + private Node endNode; + + public SimplePath(Node startNode, Node endNode, Relationship lastRelationship, int length, Iterable nodes, Iterable relationships) { + this.startNode = startNode; + this.endNode = endNode; + this.lastRelationship = lastRelationship; + this.length = length; + this.nodes = nodes; + this.relationships = relationships; + } + + public Node startNode() { + return startNode; + } + + public Node endNode() { + return endNode; + } + + public Relationship lastRelationship() { + return lastRelationship; + } + + public Iterable relationships() { + return relationships; + } + + public Iterable nodes() { + return nodes; + } + + public int length() { + return length; + } + + public Iterator iterator() { + return new Iterator() + { + Iterator current = nodes().iterator(); + Iterator next = relationships().iterator(); + + public boolean hasNext() + { + return current.hasNext(); + } + + public PropertyContainer next() + { + try + { + return current.next(); + } + finally + { + Iterator temp = current; + current = next; + next = temp; + } + } + + public void remove() + { + next.remove(); + } + }; + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestIndex.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestIndex.java new file mode 100644 index 000000000..53b5f0a94 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestIndex.java @@ -0,0 +1,137 @@ +package org.neo4j.rest.graphdb.index; + +import com.sun.jersey.api.client.ClientResponse; +import org.neo4j.graphdb.PropertyContainer; +import org.neo4j.graphdb.index.Index; +import org.neo4j.graphdb.index.IndexHits; +import org.neo4j.rest.graphdb.JsonHelper; +import org.neo4j.rest.graphdb.RestEntity; +import org.neo4j.rest.graphdb.RestGraphDatabase; +import org.neo4j.rest.graphdb.RestRequest; +import sun.reflect.generics.reflectiveObjects.NotImplementedException; + +import javax.ws.rs.core.Response; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; + +/** + * @author mh + * @since 24.01.11 + */ +public abstract class RestIndex implements Index { + private final RestRequest restRequest; + private final String indexName; + protected final RestGraphDatabase restGraphDatabase; + + RestIndex( RestRequest restRequest, String indexName, RestGraphDatabase restGraphDatabase ) { + this.restRequest = restRequest; + this.indexName = indexName; + this.restGraphDatabase = restGraphDatabase; + } + + public String getName() { + return indexName; + } + + private String getTypeName() { + return getEntityType().getSimpleName().toLowerCase(); + } + + public void add( T entity, String key, Object value ) { + String uri = ( (RestEntity) entity ).getUri(); + restRequest.post( indexPath( key, value ), JsonHelper.createJsonFrom( uri ) ); + } + + private String indexPath( String key, Object value ) { + return "index/" + getTypeName() + "/" + indexName + "/" + RestRequest.encode( key ) + "/" + RestRequest.encode( value ); + } + + public void remove( T entity, String key, Object value ) { + restRequest.delete( indexPath( key, value ) + "/" + ( (RestEntity) entity ).getId() ); + + } + + public void remove(T entity, String key) { + throw new NotImplementedException(); + } + + public void remove(T entity) { + throw new NotImplementedException(); + } + + public void delete() { + throw new NotImplementedException(); + } + + public org.neo4j.graphdb.index.IndexHits get( String key, Object value ) { + return query( key, value ); + } + + public IndexHits query( String key, Object value ) { + ClientResponse response = restRequest.get( indexPath( key, value ) ); + if ( restRequest.statusIs( response, Response.Status.OK ) ) { + Collection hits = (Collection) restRequest.toEntity( response ); + return new SimpleIndexHits( hits, hits.size() ); + } else { + return new SimpleIndexHits( Collections.emptyList(), 0 ); + } + } + + protected abstract T createEntity( Map item ); + + public org.neo4j.graphdb.index.IndexHits query( Object value ) { + throw new UnsupportedOperationException(); + } + + private class SimpleIndexHits implements IndexHits { + private Collection hits; + private int size; + private Iterator iterator; + + public SimpleIndexHits( Collection hits, int size ) { + this.hits = hits; + this.iterator = this.hits.iterator(); + this.size = size; + } + + public int size() { + return size; + } + + public void close() { + + } + + public T getSingle() { + Iterator it = hits.iterator(); + return it.hasNext() ? transform( it.next() ) : null; + } + + public float currentScore() { + return 0; + } + + public Iterator iterator() { + return this; + } + + public boolean hasNext() { + return iterator.hasNext(); + } + + public T next() { + Object value = iterator.next(); + return transform( value ); + } + + private T transform( Object value ) { + return (T) createEntity( (Map) value ); + } + + public void remove() { + + } + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestIndexManager.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestIndexManager.java new file mode 100644 index 000000000..62d71e172 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestIndexManager.java @@ -0,0 +1,77 @@ +package org.neo4j.rest.graphdb.index; + +import com.sun.jersey.api.client.ClientResponse; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.PropertyContainer; +import org.neo4j.graphdb.index.Index; +import org.neo4j.graphdb.index.IndexManager; +import org.neo4j.graphdb.index.RelationshipIndex; +import org.neo4j.rest.graphdb.RestGraphDatabase; +import org.neo4j.rest.graphdb.RestRequest; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +public class RestIndexManager implements IndexManager { + private RestRequest restRequest; + private RestGraphDatabase restGraphDatabase; + + public RestIndexManager( RestRequest restRequest, RestGraphDatabase restGraphDatabase ) { + this.restRequest = restRequest; + this.restGraphDatabase = restGraphDatabase; + } + + public boolean existsForNodes( String indexName ) { + return indexInfo( "node" ).containsKey( indexName ); + } + + private Map indexInfo( final String indexType ) { + ClientResponse response = restRequest.get( "index/" + indexType ); + if ( restRequest.statusIs( response, ClientResponse.Status.NO_CONTENT ) ) return Collections.emptyMap(); + return (Map) restRequest.toMap( response ); + } + + public Index forNodes( String indexName ) { + return new RestNodeIndex( restRequest, indexName, restGraphDatabase ); + } + + public Index forNodes( String indexName, Map stringStringMap ) { + return new RestNodeIndex( restRequest, indexName, restGraphDatabase ); + } + + public String[] nodeIndexNames() { + Set keys = indexInfo( "node" ).keySet(); + return keys.toArray( new String[keys.size()] ); + } + + public boolean existsForRelationships( String indexName ) { + return indexInfo( "relationship" ).containsKey( indexName ); + } + + public RelationshipIndex forRelationships( String indexName ) { + return new RestRelationshipIndex( restRequest, indexName, restGraphDatabase ); + } + + public RelationshipIndex forRelationships( String indexName, Map stringStringMap ) { + return new RestRelationshipIndex( restRequest, indexName, restGraphDatabase ); + } + + public String[] relationshipIndexNames() { + Set keys = indexInfo( "relationship" ).keySet(); + return keys.toArray( new String[keys.size()] ); + } + + public Map getConfiguration( Index index ) { + return null; + } + + public String setConfiguration( Index index, String s, String s1 ) { + return null; + } + + public String removeConfiguration( Index index, String s ) { + return null; + } +} + diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestNodeIndex.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestNodeIndex.java new file mode 100644 index 000000000..1c2a15380 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestNodeIndex.java @@ -0,0 +1,34 @@ +package org.neo4j.rest.graphdb.index; + +import org.neo4j.graphdb.Node; +import org.neo4j.rest.graphdb.RestGraphDatabase; +import org.neo4j.rest.graphdb.RestNode; +import org.neo4j.rest.graphdb.RestRequest; + +import java.util.Map; + +/** + * @author mh + * @since 24.01.11 + */ +public class RestNodeIndex extends RestIndex { + public RestNodeIndex( RestRequest restRequest, String indexName, RestGraphDatabase restGraphDatabase ) { + super( restRequest, indexName, restGraphDatabase ); + } + + public Class getEntityType() { + return Node.class; + } + + public void remove(Node entity, String key) { + throw new UnsupportedOperationException(); + } + + public void remove(Node entity) { + throw new UnsupportedOperationException(); + } + + protected Node createEntity(Map item) { + return new RestNode((Map) item, restGraphDatabase); + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestRelationshipIndex.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestRelationshipIndex.java new file mode 100644 index 000000000..bf817329c --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/index/RestRelationshipIndex.java @@ -0,0 +1,48 @@ +package org.neo4j.rest.graphdb.index; + +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.index.RelationshipIndex; +import org.neo4j.rest.graphdb.RestGraphDatabase; +import org.neo4j.rest.graphdb.RestRelationship; +import org.neo4j.rest.graphdb.RestRequest; + +import java.util.Map; + +/** + * @author mh + * @since 24.01.11 + */ +public class RestRelationshipIndex extends RestIndex implements RelationshipIndex { + public RestRelationshipIndex( RestRequest restRequest, String indexName, RestGraphDatabase restGraphDatabase ) { + super( restRequest, indexName, restGraphDatabase ); + } + + public Class getEntityType() { + return Relationship.class; + } + + public void remove(Relationship entity, String key) { + throw new UnsupportedOperationException(); + } + + public void remove(Relationship entity) { + throw new UnsupportedOperationException(); + } + + protected Relationship createEntity( Map item ) { + return new RestRelationship( (Map) item, restGraphDatabase ); + } + + public org.neo4j.graphdb.index.IndexHits get( String s, Object o, Node node, Node node1 ) { + throw new UnsupportedOperationException(); + } + + public org.neo4j.graphdb.index.IndexHits query( String s, Object o, Node node, Node node1 ) { + throw new UnsupportedOperationException(); + } + + public org.neo4j.graphdb.index.IndexHits query( Object o, Node node, Node node1 ) { + throw new UnsupportedOperationException(); + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/ArrayConverter.java b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/ArrayConverter.java new file mode 100644 index 000000000..351aa7121 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/neo4j/rest/graphdb/util/ArrayConverter.java @@ -0,0 +1,62 @@ +package org.neo4j.rest.graphdb.util; + +import java.lang.reflect.Array; +import java.util.Collection; + +/** + * @author Michael Hunger + * @since 02.02.11 + */ +public class ArrayConverter { + public Object toArray(Collection col) { + Object entry = getNonNullEntry(col); + if (entry==null) return null; + Class elementClass = getArrayElementClass( entry ); + Object array = Array.newInstance(elementClass, col.size()); + if (Object.class.isAssignableFrom( elementClass)) { + col.toArray( (Object[])array ); + } else { + int i=0; + for ( Object value : col ) { + setArrayValue(array,i,value,elementClass); + i+=1; + } + } + return array; + } + + private void setArrayValue( Object array, int i, Object value, Class type ) { + if (value==null) return; + if ( value instanceof Number ) { + Number number = (Number) value; + if (type.equals( int.class )) { Array.setInt( array, i, number.intValue()); return;} + if (type.equals( long.class )) { Array.setLong( array, i, number.longValue()); return;} + if (type.equals( double.class )) { Array.setDouble( array, i, number.doubleValue()); return;} + if (type.equals( float.class )) { Array.setFloat( array, i, number.floatValue()); return;} + if (type.equals( byte.class )) { Array.setByte( array, i, number.byteValue()); return;} + if (type.equals( short.class )) { Array.setShort( array, i, number.shortValue()); return;} + } + if (type.equals( char.class )) { Array.setChar( array, i, (Character)value ); return;} + if (type.equals( boolean.class )) { Array.setBoolean( array, i, (Boolean) value ); return;} + } + + private Class getArrayElementClass( Object entry ) { + Class type = entry.getClass(); + if (type.equals( Integer.class )) return int.class; + if (type.equals( Long.class )) return long.class; + if (type.equals( Double.class )) return double.class; + if (type.equals( Float.class )) return float.class; + if (type.equals( Byte.class )) return byte.class; + if (type.equals( Short.class )) return short.class; + if (type.equals( Character.class )) return char.class; + if (type.equals( Boolean.class )) return boolean.class; + return type; + } + + private Object getNonNullEntry(Collection col) { + for ( Object entry : col ) { + if (entry!=null) return entry; + } + return null; + } +} diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/IsRelationshipToNodeMatcher.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/IsRelationshipToNodeMatcher.java new file mode 100644 index 000000000..dd7014c92 --- /dev/null +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/IsRelationshipToNodeMatcher.java @@ -0,0 +1,36 @@ +package org.neo4j.rest.graphdb; + +import org.hamcrest.Description; +import org.junit.internal.matchers.TypeSafeMatcher; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; + +/** + * @author mh + * @since 24.01.11 + */ +class IsRelationshipToNodeMatcher extends TypeSafeMatcher> { + private final Node startNode; + private final Node endNode; + + public IsRelationshipToNodeMatcher( Node startNode, Node endNode ) { + this.startNode = startNode; + this.endNode = endNode; + } + + @Override + public boolean matchesSafely( Iterable relationships ) { + return relationshipFromTo( relationships, startNode, endNode ) != null; + } + + public static Relationship relationshipFromTo( Iterable relationships, final Node startNode, final Node endNode ) { + for ( Relationship relationship : relationships ) { + if ( relationship.getOtherNode( startNode ).equals( endNode ) ) return relationship; + } + return null; + } + + public void describeTo( Description description ) { + description.appendValue( startNode ).appendText( " to " ).appendValue( endNode ).appendText( "not contained in relationships" ); + } +} diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/LocalTestServer.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/LocalTestServer.java new file mode 100644 index 000000000..0ab2ebc85 --- /dev/null +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/LocalTestServer.java @@ -0,0 +1,96 @@ +package org.neo4j.rest.graphdb; + +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.kernel.AbstractGraphDatabase; +import org.neo4j.server.AddressResolver; +import org.neo4j.server.NeoServerWithEmbeddedWebServer; +import org.neo4j.server.database.Database; +import org.neo4j.server.modules.RESTApiModule; +import org.neo4j.server.modules.ThirdPartyJAXRSModule; +import org.neo4j.server.startup.healthcheck.StartupHealthCheck; +import org.neo4j.server.web.Jetty6WebServer; + +import java.io.File; +import java.net.URI; +import java.net.URL; + +/** + * @author mh + * @since 24.03.11 + */ +public class LocalTestServer { + private NeoServerWithEmbeddedWebServer neoServer; + private final int port; + private final String hostname; + protected String propertiesFile = "test-db.properties"; + + public LocalTestServer() { + this("localhost",7473); + } + + public LocalTestServer(String hostname, int port) { + this.port = port; + this.hostname = hostname; + } + + public void start() { + if (neoServer!=null) throw new IllegalStateException("Server already running"); + URL url = getClass().getResource("/" + propertiesFile); + if (url==null) throw new IllegalArgumentException("Could not resolve properties file "+propertiesFile); + neoServer = new NeoServerWithEmbeddedWebServer(new AddressResolver() { + @Override + public String getHostname() { + return hostname; + } + }, new StartupHealthCheck(), new File(url.getPath()), new Jetty6WebServer()) { + protected void registerServerModules() { + registerModule(RESTApiModule.class); + registerModule(ThirdPartyJAXRSModule.class); + } + + @Override + protected int getWebServerPort() { + return port; + } + }; + neoServer.start(); + } + + public void stop() { + try { + neoServer.stop(); + } catch(Exception e) { + System.err.println("Error stopping server: "+e.getMessage()); + } + neoServer=null; + } + + public int getPort() { + return port; + } + + public String getHostname() { + return hostname; + } + + public LocalTestServer withPropertiesFile(String propertiesFile) { + this.propertiesFile = propertiesFile; + return this; + } + public Database getDatabase() { + return neoServer.getDatabase(); + } + + public URI baseUri() { + return neoServer.baseUri(); + } + + public void cleanDb() { + Neo4jDatabaseCleaner cleaner = new Neo4jDatabaseCleaner(getGraphDatabase()); + cleaner.cleanDb(); + } + + public AbstractGraphDatabase getGraphDatabase() { + return getDatabase().graph; + } +} diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/Neo4jDatabaseCleaner.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/Neo4jDatabaseCleaner.java new file mode 100644 index 000000000..79fa8a08e --- /dev/null +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/Neo4jDatabaseCleaner.java @@ -0,0 +1,67 @@ +package org.neo4j.rest.graphdb; + +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.Transaction; +import org.neo4j.graphdb.index.IndexManager; +import org.neo4j.kernel.AbstractGraphDatabase; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * @author mh + * @since 02.03.11 + */ +public class Neo4jDatabaseCleaner { + private AbstractGraphDatabase graph; + + public Neo4jDatabaseCleaner(AbstractGraphDatabase graph) { + this.graph = graph; + } + + public Map cleanDb() { + Map result = new HashMap(); + Transaction tx = graph.beginTx(); + try { + removeNodes(result); + clearIndex(result); + tx.success(); + } finally { + tx.finish(); + } + return result; + } + + private void removeNodes(Map result) { + Node refNode = graph.getReferenceNode(); + int nodes = 0, relationships = 0; + for (Node node : graph.getAllNodes()) { + for (Relationship rel : node.getRelationships(Direction.OUTGOING)) { + rel.delete(); + relationships++; + } + if (!refNode.equals(node)) { + node.delete(); + nodes++; + } + } + result.put("nodes", nodes); + result.put("relationships", relationships); + + } + + private void clearIndex(Map result) { + IndexManager indexManager = graph.index(); + result.put("node-indexes", Arrays.asList(indexManager.nodeIndexNames())); + result.put("relationship-indexes", Arrays.asList(indexManager.relationshipIndexNames())); + for (String ix : indexManager.nodeIndexNames()) { + indexManager.forNodes(ix).delete(); + } + for (String ix : indexManager.relationshipIndexNames()) { + indexManager.forRelationships(ix).delete(); + } + } +} diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestEntityTest.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestEntityTest.java new file mode 100644 index 000000000..b3ebba452 --- /dev/null +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestEntityTest.java @@ -0,0 +1,70 @@ +package org.neo4j.rest.graphdb; + +import org.junit.Assert; +import org.junit.Test; +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; + +import java.util.Arrays; + +public class RestEntityTest extends RestTestBase { + + @Test + public void testSetProperty() { + graphDb.getReferenceNode().setProperty( "name", "test" ); + Node node = graphDb.getReferenceNode(); + Assert.assertEquals( "test", node.getProperty( "name" ) ); + } + + @Test + public void testSetStringArrayProperty() { + graphDb.getReferenceNode().setProperty( "name", new String[]{"test"} ); + Node node = graphDb.getReferenceNode(); + Assert.assertArrayEquals( new String[]{"test"}, (String[])node.getProperty( "name" ) ); + } + @Test + public void testSetDoubleArrayProperty() { + double[] data = {0, 1, 2}; + graphDb.getReferenceNode().setProperty( "data", data ); + Node node = graphDb.getReferenceNode(); + Assert.assertTrue("same double array",Arrays.equals( data, (double[])node.getProperty( "data" ) )); + } + + @Test + public void testRemoveProperty() { + Node node = graphDb.getReferenceNode(); + node.setProperty( "name", "test" ); + Assert.assertEquals( "test", node.getProperty( "name" ) ); + node.removeProperty( "name" ); + Assert.assertEquals( false, node.hasProperty( "name" ) ); + } + + + @Test + public void testSetPropertyOnRelationship() { + Node refNode = graphDb.getReferenceNode(); + Node node = graphDb.createNode(); + Relationship rel = refNode.createRelationshipTo( node, Type.TEST ); + rel.setProperty( "name", "test" ); + Assert.assertEquals( "test", rel.getProperty( "name" ) ); + Relationship foundRelationship = IsRelationshipToNodeMatcher.relationshipFromTo( refNode.getRelationships( Type.TEST, Direction.OUTGOING ), refNode, node ); + Assert.assertEquals( "test", foundRelationship.getProperty( "name" ) ); + } + + @Test + public void testRemovePropertyOnRelationship() { + Node refNode = graphDb.getReferenceNode(); + Node node = graphDb.createNode(); + Relationship rel = refNode.createRelationshipTo( node, Type.TEST ); + rel.setProperty( "name", "test" ); + Assert.assertEquals( "test", rel.getProperty( "name" ) ); + Relationship foundRelationship = IsRelationshipToNodeMatcher.relationshipFromTo( refNode.getRelationships( Type.TEST, Direction.OUTGOING ), refNode, node ); + Assert.assertEquals( "test", foundRelationship.getProperty( "name" ) ); + rel.removeProperty( "name" ); + Assert.assertEquals( false, rel.hasProperty( "name" ) ); + Relationship foundRelationship2 = IsRelationshipToNodeMatcher.relationshipFromTo( refNode.getRelationships( Type.TEST, Direction.OUTGOING ), refNode, node ); + Assert.assertEquals( false, foundRelationship2.hasProperty( "name" ) ); + } + +} diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestGraphDbTest.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestGraphDbTest.java new file mode 100644 index 000000000..1cd2c1b42 --- /dev/null +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestGraphDbTest.java @@ -0,0 +1,56 @@ +package org.neo4j.rest.graphdb; + +import org.junit.Assert; +import org.junit.Test; +import org.neo4j.graphdb.*; + +import java.util.Date; + +public class RestGraphDbTest extends RestTestBase { + + @Test + public void testGetRefNode() { + Node refNode = graphDb.getReferenceNode(); + Node nodeById = graphDb.getNodeById( 0 ); + Assert.assertEquals( refNode, nodeById ); + } + + @Test + public void testCreateNode() { + Node node = graphDb.createNode(); + Assert.assertEquals( node, graphDb.getNodeById( node.getId() ) ); + } + + @Test + public void testCreateRelationship() { + Node refNode = graphDb.getReferenceNode(); + Node node = graphDb.createNode(); + Relationship rel = refNode.createRelationshipTo( node, Type.TEST ); + Relationship foundRelationship = IsRelationshipToNodeMatcher.relationshipFromTo( refNode.getRelationships( Type.TEST, Direction.OUTGOING ), refNode, node ); + Assert.assertNotNull( "found relationship", foundRelationship ); + Assert.assertEquals( "same relationship", rel, foundRelationship ); + Assert.assertThat( refNode.getRelationships( Type.TEST, Direction.OUTGOING ), new IsRelationshipToNodeMatcher( refNode, node ) ); + Assert.assertThat( refNode.getRelationships( Direction.OUTGOING ), new IsRelationshipToNodeMatcher( refNode, node ) ); + Assert.assertThat( refNode.getRelationships( Direction.BOTH ), new IsRelationshipToNodeMatcher( refNode, node ) ); + Assert.assertThat( refNode.getRelationships( Type.TEST ), new IsRelationshipToNodeMatcher( refNode, node ) ); + } + + @Test + public void testBasic() { + Node refNode = graphDb.getReferenceNode(); + Node node = graphDb.createNode(); + Relationship rel = refNode.createRelationshipTo( node, + DynamicRelationshipType.withName( "TEST" ) ); + rel.setProperty( "date", new Date().getTime() ); + node.setProperty( "name", "Mattias test" ); + refNode.createRelationshipTo( node, + DynamicRelationshipType.withName( "TEST" ) ); + + for ( Relationship relationship : refNode.getRelationships() ) { + System.out.println( "rel prop:" + relationship.getProperty( "date", null ) ); + Node endNode = relationship.getEndNode(); + System.out.println( "node prop:" + endNode.getProperty( "name", null ) ); + } + } + +} diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestIndexTest.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestIndexTest.java new file mode 100644 index 000000000..bff420145 --- /dev/null +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestIndexTest.java @@ -0,0 +1,93 @@ +package org.neo4j.rest.graphdb; + +import org.junit.Assert; +import org.junit.Test; +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.index.Index; +import org.neo4j.graphdb.index.IndexHits; +import org.neo4j.graphdb.index.RelationshipIndex; + +import java.util.Arrays; +import java.util.Iterator; + +public class RestIndexTest extends RestTestBase { + + private static final String NODE_INDEX_NAME = "NODE_INDEX"; + private static final String REL_INDEX_NAME = "REL_INDEX"; + + @Test + public void testAddToNodeIndex() { + nodeIndex().add(node(), "name", "test"); + IndexHits hits = nodeIndex().get("name", "test"); + Assert.assertEquals("index results", true, hits.hasNext()); + Assert.assertEquals(node(), hits.next()); + } + + @Test + public void testNotFoundInNodeIndex() { + IndexHits hits = nodeIndex().get("foo", "bar"); + Assert.assertEquals("no index results", false, hits.hasNext()); + } + + @Test + public void testAddToRelationshipIndex() { + final long value = System.currentTimeMillis(); + relationshipIndex().add(relationship(), "name", value); + IndexHits hits = relationshipIndex().get("name", value); + Assert.assertEquals("index results", true, hits.hasNext()); + Assert.assertEquals(relationship(), hits.next()); + } + + @Test + public void testNotFoundInRelationshipIndex() { + IndexHits hits = relationshipIndex().get("foo", "bar"); + Assert.assertEquals("no index results", false, hits.hasNext()); + } + + @Test + public void testDeleteFromNodeIndex() { + String value = String.valueOf(System.currentTimeMillis()); + nodeIndex().add(node(), "time", value); + IndexHits hits = nodeIndex().get("time", value); + Assert.assertEquals("found in index results", true, hits.hasNext()); + Assert.assertEquals("found in index results", node(), hits.next()); + nodeIndex().remove(node(), "time", value); + IndexHits hitsAfterRemove = nodeIndex().get("time", value); + Assert.assertEquals("not found in index results", false, hitsAfterRemove.hasNext()); + } + + @Test + public void testDeleteFromRelationshipIndex() { + String value = String.valueOf(System.currentTimeMillis()); + relationshipIndex().add(relationship(), "time", value); + IndexHits hits = relationshipIndex().get("time", value); + Assert.assertEquals("found in index results", true, hits.hasNext()); + Assert.assertEquals("found in index results", relationship(), hits.next()); + relationshipIndex().remove(relationship(), "time", value); + IndexHits hitsAfterRemove = relationshipIndex().get("time", value); + Assert.assertEquals("not found in index results", false, hitsAfterRemove.hasNext()); + } + + private Index nodeIndex() { + return graphDb.index().forNodes(NODE_INDEX_NAME); + } + + private RelationshipIndex relationshipIndex() { + return graphDb.index().forRelationships(REL_INDEX_NAME); + } + + @Test + public void testNodeIndexIsListed() { + nodeIndex().add(node(), "name", "test"); + Assert.assertTrue("node index name listed", Arrays.asList(graphDb.index().nodeIndexNames()).contains(NODE_INDEX_NAME)); + } + + @Test + public void testRelationshipIndexIsListed() { + relationshipIndex().add(relationship(), "name", "test"); + Assert.assertTrue("relationship index name listed", Arrays.asList(graphDb.index().relationshipIndexNames()).contains(REL_INDEX_NAME)); + } + +} diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTestBase.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTestBase.java new file mode 100644 index 000000000..3a129e724 --- /dev/null +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTestBase.java @@ -0,0 +1,71 @@ +package org.neo4j.rest.graphdb; + + +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientResponse; +import org.apache.log4j.BasicConfigurator; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; +import org.neo4j.server.AddressResolver; +import org.neo4j.server.NeoServerWithEmbeddedWebServer; +import org.neo4j.server.modules.RESTApiModule; +import org.neo4j.server.modules.ThirdPartyJAXRSModule; +import org.neo4j.server.startup.healthcheck.StartupHealthCheck; +import org.neo4j.server.web.Jetty6WebServer; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Iterator; + +public class RestTestBase { + + protected GraphDatabaseService graphDb; + private static final String HOSTNAME = "localhost"; + private static final int PORT = 7474; + private static LocalTestServer neoServer = new LocalTestServer(HOSTNAME,PORT).withPropertiesFile("test-db.properties"); + private static final String SERVER_ROOT_URI = "http://" + HOSTNAME + ":" + PORT + "/db/data/"; + private static final String SERVER_CLEANDB_URI = "http://" + HOSTNAME + ":" + PORT + "/cleandb/secret-key"; + + @BeforeClass + public static void startDb() throws Exception { + BasicConfigurator.configure(); + neoServer.start(); + } + + @Before + public void setUp() throws URISyntaxException { + cleanDb(); + graphDb = new RestGraphDatabase(new URI(SERVER_ROOT_URI)); + } + + private void cleanDb() { + neoServer.cleanDb(); + } + + @After + public void tearDown() throws Exception { + graphDb.shutdown(); + } + + @AfterClass + public static void shutdownDb() { + neoServer.stop(); + + } + + protected Relationship relationship() { + Iterator it = node().getRelationships(Direction.OUTGOING).iterator(); + if (it.hasNext()) return it.next(); + return node().createRelationshipTo(graphDb.createNode(), Type.TEST); + } + + protected Node node() { + return graphDb.getReferenceNode(); + } +} diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTraversalExecutionTest.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTraversalExecutionTest.java new file mode 100644 index 000000000..a386fb2a2 --- /dev/null +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTraversalExecutionTest.java @@ -0,0 +1,21 @@ +package org.neo4j.rest.graphdb; + +import org.junit.Assert; +import org.junit.Test; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.traversal.TraversalDescription; +import org.neo4j.graphdb.traversal.Traverser; + +public class RestTraversalExecutionTest extends RestTestBase { + @Test + public void testTraverseToNeighbour() { + final Relationship rel = relationship(); + final TraversalDescription traversalDescription = RestTraversal.description().maxDepth(1).breadthFirst(); + System.out.println("traversalDescription = " + traversalDescription); + final Traverser traverser = traversalDescription.traverse(rel.getStartNode()); + final Iterable nodes = traverser.nodes(); + Assert.assertEquals(rel.getEndNode(), nodes.iterator().next()); + } + +} diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTraversalTest.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTraversalTest.java new file mode 100644 index 000000000..cc281774f --- /dev/null +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/RestTraversalTest.java @@ -0,0 +1,111 @@ +package org.neo4j.rest.graphdb; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.neo4j.graphdb.traversal.TraversalDescription; +import org.neo4j.kernel.Uniqueness; + +import java.util.Map; + +/** + * @author Michael Hunger + * @since 03.02.11 + */ +public class RestTraversalTest { + private RestTraversal traversalDescription; + + @Before + public void setUp() throws Exception { + traversalDescription = (RestTraversal) RestTraversal.description(); + } + + @Test + public void testUniqueness() throws Exception { + traversalDescription.uniqueness(Uniqueness.NODE_PATH); + Assert.assertEquals("node path", getPostData("uniqueness")); + } + + @Test + public void testUniquenessWithValue() throws Exception { + traversalDescription.uniqueness(Uniqueness.NODE_PATH,"test"); + String param = "uniqueness"; + final Map uniquenessMap = (Map) getPostData(param); + Assert.assertEquals("node path", uniquenessMap.get("name")); + Assert.assertEquals("test", uniquenessMap.get("value")); + } + + private Object getPostData(String param) { + return traversalDescription.getPostData().get(param); + } + + @Test + public void testPruneScript() throws Exception { + traversalDescription.prune(RestTraversalDescription.ScriptLanguage.JAVASCRIPT, "return true;"); + Map pruneEvaluator= (Map) getPostData("prune evaluator"); + Assert.assertEquals("javascript", pruneEvaluator.get("language")); + Assert.assertEquals("return true;", pruneEvaluator.get("body")); + } + + @Test + public void testFilterScript() throws Exception { + traversalDescription.filter(RestTraversalDescription.ScriptLanguage.JAVASCRIPT, "return true;"); + Map pruneEvaluator= (Map) getPostData("return filter"); + Assert.assertEquals("javascript", pruneEvaluator.get("language")); + Assert.assertEquals("return true;", pruneEvaluator.get("body")); + } + + @Test + public void testEvaluator() throws Exception { + + } + + @Test + public void testPrune() throws Exception { + + } + + @Test + public void testFilter() throws Exception { + + } + + @Test + public void testMaxDepth() throws Exception { + + } + + @Test + public void testOrder() throws Exception { + + } + + @Test + public void testDepthFirst() throws Exception { + + } + + @Test + public void testBreadthFirst() throws Exception { + + } + + @Test + public void testRelationships() throws Exception { + + } + + @Test + public void testRelationshipsAndDirection() throws Exception { + + } + + @Test + public void testExpand() throws Exception { + + } + @Test + public void testComplexTraversal() throws Exception { + + } +} diff --git a/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/Type.java b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/Type.java new file mode 100644 index 000000000..e30840bef --- /dev/null +++ b/spring-data-neo4j-rest/src/test/java/org/neo4j/rest/graphdb/Type.java @@ -0,0 +1,11 @@ +package org.neo4j.rest.graphdb; + +import org.neo4j.graphdb.RelationshipType; + +/** + * @author mh + * @since 24.01.11 + */ +enum Type implements RelationshipType { + TEST +} diff --git a/spring-data-neo4j-rest/src/test/resources/log4j.properties b/spring-data-neo4j-rest/src/test/resources/log4j.properties new file mode 100644 index 000000000..e69fbde1b --- /dev/null +++ b/spring-data-neo4j-rest/src/test/resources/log4j.properties @@ -0,0 +1,26 @@ +log4j.rootLogger=WARN, stdout, R + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout + +# Print the date in ISO 8601 format +log4j.appender.stdout.layout.ConversionPattern=%d [%t] %-5p %c - %m%n + +log4j.appender.R=org.apache.log4j.RollingFileAppender +log4j.appender.R.File=target/application.log + +log4j.appender.R.MaxFileSize=100KB +# Keep one backup file +log4j.appender.R.MaxBackupIndex=1 + +log4j.appender.R.layout=org.apache.log4j.PatternLayout +log4j.appender.R.layout.ConversionPattern=%p %t %c - %m%n + + +log4j.category.org.springframework=WARN +#log4j.category.org.springframework.data.graph.neo4j.support.SubReferenceNodeTypeStrategy=DEBUG +#log4j.category.org.springframework.data.graph.neo4j.fieldaccess=DEBUG +#log4j.category.org.springframework.data=TRACE +#log4j.category.org.springframework.data.support=TRACE +#log4j.category.org.springframework.persistence=TRACE +#log4j.category.org.springframework.data.graph.neo4j.support=DEBUG diff --git a/spring-data-neo4j-rest/src/test/resources/test-db.properties b/spring-data-neo4j-rest/src/test/resources/test-db.properties new file mode 100644 index 000000000..64291a371 --- /dev/null +++ b/spring-data-neo4j-rest/src/test/resources/test-db.properties @@ -0,0 +1 @@ +org.neo4j.server.database.location=target/test-db