diff --git a/changelog.txt b/changelog.txt index ad53cdf21..89b3a87ca 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,13 @@ Spring Data Graph Changelog =========================== +Changes in version 1.1.0.RC1 (2011-07-25) +----------------------------------------- +* Added Gremlin support (embedded & REST) +* QueryEngine.query method now takes a parameter map (for cypher and gremlin) +* documentation updates + + Changes in version 1.1.0.M2 (2011-07-20) ----------------------------------------- * updated dependency to Neo4j 1.4 diff --git a/spring-data-graph-parent/pom.xml b/spring-data-graph-parent/pom.xml index d625125b6..0f226e868 100644 --- a/spring-data-graph-parent/pom.xml +++ b/spring-data-graph-parent/pom.xml @@ -15,9 +15,11 @@ 1.8.4 1.5.10 3.0.5.RELEASE - 1.1.0.RC1 + 1.1.0.RELEASE 1.4 1.6.12.M1 + 0.8 + 1.1 @@ -184,6 +186,27 @@ runtime + + + com.tinkerpop.blueprints + blueprints-core + ${blueprints.version} + optional + + + com.tinkerpop.blueprints + blueprints-neo4j-graph + ${blueprints.version} + optional + + + + com.tinkerpop + gremlin + ${gremlin.version} + optional + + javax.annotation jsr250-api diff --git a/spring-data-neo4j-rest/pom.xml b/spring-data-neo4j-rest/pom.xml index 5f027d10f..a695d3384 100644 --- a/spring-data-neo4j-rest/pom.xml +++ b/spring-data-neo4j-rest/pom.xml @@ -58,6 +58,13 @@ org.neo4j.server.plugin neo4j-cypher-plugin ${neo4j.version} + test + + + org.neo4j.server.plugin + neo4j-gremlin-plugin + ${neo4j.version} + test org.neo4j.app diff --git a/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/JsonHelper.java b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/JsonHelper.java index 4f8f0eca0..abe8834ea 100644 --- a/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/JsonHelper.java +++ b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/JsonHelper.java @@ -44,7 +44,7 @@ public class JsonHelper { return (List>) readJson( json ); } - private static Object readJson( String json ) { + public static Object readJson( String json ) { ObjectMapper mapper = new ObjectMapper(); try { return mapper.readValue( json, Object.class ); diff --git a/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestCypherQueryEngine.java b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestCypherQueryEngine.java index c55edd66e..219b8d462 100644 --- a/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestCypherQueryEngine.java +++ b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestCypherQueryEngine.java @@ -17,16 +17,19 @@ package org.springframework.data.neo4j.rest; +import org.neo4j.helpers.collection.MapUtil; import org.springframework.data.neo4j.conversion.*; import org.springframework.data.neo4j.support.query.QueryEngine; -import java.util.*; +import java.util.Iterator; +import java.util.List; +import java.util.Map; /** * @author mh * @since 22.06.11 */ -public class RestCypherQueryEngine implements QueryEngine { +public class RestCypherQueryEngine implements QueryEngine> { private final RestRequest restRequest; private final RestGraphDatabase restGraphDatabase; private final ResultConverter resultConverter; @@ -41,19 +44,14 @@ public class RestCypherQueryEngine implements QueryEngine { } @Override - public QueryResult> query(String statement) { - return executeStatement(statement); - } - - private RestQueryResult executeStatement(String statement) { - final RequestResult requestResult = restRequest.get("ext/CypherPlugin/graphdb/execute_query", JsonHelper.createJsonFrom(Collections.singletonMap("query", statement))); + public QueryResult> query(String statement, Map params) { + final String parametrizedStatement = QueryResultBuilder.replaceParams(statement, params); + final RequestResult requestResult = restRequest.get("ext/CypherPlugin/graphdb/execute_query", JsonHelper.createJsonFrom(MapUtil.map("query", parametrizedStatement))); return new RestQueryResult(restRequest.toMap(requestResult),restGraphDatabase,resultConverter); } static class RestQueryResult implements QueryResult> { QueryResultBuilder> result; - private final RestGraphDatabase restGraphDatabase; - @Override public ConvertedResult to(Class type) { @@ -76,49 +74,9 @@ public class RestCypherQueryEngine implements QueryEngine { } public RestQueryResult(Map result, RestGraphDatabase restGraphDatabase, ResultConverter resultConverter) { - this.restGraphDatabase = restGraphDatabase; - List columns= (List) result.get("columns"); - final List> data = extractData(result, columns); + final RestTableResultExtractor extractor = new RestTableResultExtractor(new RestEntityExtractor(restGraphDatabase)); + final List> data = extractor.extract(result); this.result=new QueryResultBuilder>(data, resultConverter); } - - private List> extractData(Map restResult, List columns) { - List> rows= (List>) restResult.get("data"); - List> result=new ArrayList>(rows.size()); - for (List row : rows) { - result.add(mapRow(columns,row)); - } - return result; - } - - private Map mapRow(List columns, List row) { - int columnCount=columns.size(); - Map newRow=new HashMap(columnCount); - for (int i = 0; i < columnCount; i++) { - final Object value = row.get(i); - newRow.put(columns.get(i), convertFromRepresentation(value)); - } - return newRow; - } - - private Object convertFromRepresentation(Object value) { - if (value instanceof Map) { - RestEntity restEntity = createRestEntity((Map) value); - if (restEntity!=null) return restEntity; - } - return value; - } - - private RestEntity createRestEntity(Map data) { - final String uri = (String) data.get("self"); - if (uri == null || uri.isEmpty()) return null; - if (uri.contains("/node/")) { - return new RestNode(data,restGraphDatabase); - } - if (uri.contains("/relationship/")) { - return new RestRelationship(data,restGraphDatabase); - } - return null; - } } } diff --git a/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestEntityExtractor.java b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestEntityExtractor.java new file mode 100644 index 000000000..4ab31703b --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestEntityExtractor.java @@ -0,0 +1,47 @@ +/** + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.neo4j.rest; + +import java.util.Map; + +public class RestEntityExtractor { + private final RestGraphDatabase restGraphDatabase; + + public RestEntityExtractor(RestGraphDatabase restGraphDatabase) { + this.restGraphDatabase = restGraphDatabase; + } + + Object convertFromRepresentation(Object value) { + if (value instanceof Map) { + RestEntity restEntity = createRestEntity((Map) value); + if (restEntity != null) return restEntity; + } + return value; + } + + RestEntity createRestEntity(Map data) { + final String uri = (String) data.get("self"); + if (uri == null || uri.isEmpty()) return null; + if (uri.contains("/node/")) { + return new RestNode(data, restGraphDatabase); + } + if (uri.contains("/relationship/")) { + return new RestRelationship(data, restGraphDatabase); + } + return null; + } +} \ No newline at end of file diff --git a/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestGraphDatabase.java b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestGraphDatabase.java index 3d4fd372d..7cf6db679 100644 --- a/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestGraphDatabase.java +++ b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestGraphDatabase.java @@ -23,6 +23,7 @@ import org.neo4j.graphdb.event.TransactionEventHandler; import org.neo4j.graphdb.index.Index; import org.neo4j.graphdb.traversal.TraversalDescription; import org.springframework.core.convert.ConversionService; +import org.springframework.data.neo4j.annotation.QueryType; import org.springframework.data.neo4j.conversion.ResultConverter; import org.springframework.data.neo4j.core.GraphDatabase; import org.springframework.data.neo4j.rest.index.RestIndexManager; @@ -103,8 +104,12 @@ public class RestGraphDatabase implements GraphDatabaseService, GraphDatabase { } @Override - public QueryEngine queryEngineFor(QueryEngine.Type type) { - return new RestCypherQueryEngine(this, createResultConverter()); + public QueryEngine queryEngineFor(QueryType type) { + switch (type) { + case Cypher: return new RestCypherQueryEngine(this, createResultConverter()); + case Gremlin: return new RestGremlinQueryEngine(this, createResultConverter()); + } + throw new IllegalArgumentException("Unknown Query Engine Type "+type); } private ResultConverter createResultConverter() { diff --git a/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestGremlinQueryEngine.java b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestGremlinQueryEngine.java new file mode 100644 index 000000000..d86ece867 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestGremlinQueryEngine.java @@ -0,0 +1,109 @@ +/** + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.neo4j.rest; + + +import org.neo4j.helpers.collection.IterableWrapper; +import org.neo4j.helpers.collection.MapUtil; +import org.springframework.data.neo4j.conversion.*; +import org.springframework.data.neo4j.support.query.QueryEngine; + +import java.util.*; + +/** + * @author mh + * @since 22.06.11 + */ +public class RestGremlinQueryEngine implements QueryEngine { + private final RestRequest restRequest; + private final RestGraphDatabase restGraphDatabase; + private final ResultConverter resultConverter; + + public RestGremlinQueryEngine(RestGraphDatabase restGraphDatabase) { + this(restGraphDatabase,null); + } + public RestGremlinQueryEngine(RestGraphDatabase restGraphDatabase, ResultConverter resultConverter) { + this.restGraphDatabase = restGraphDatabase; + this.resultConverter = resultConverter!=null ? resultConverter : new DefaultConverter(); + this.restRequest = restGraphDatabase.getRestRequest(); + } + + @Override + public QueryResult query(String statement, Map params) { + final String data = JsonHelper.createJsonFrom(MapUtil.map("script", statement,"params",params)); + final RequestResult requestResult = restRequest.get("ext/GremlinPlugin/graphdb/execute_script", data); + return new RestQueryResult(JsonHelper.readJson(requestResult.getEntity()),restGraphDatabase,resultConverter); + } + + static class RestQueryResult implements QueryResult { + QueryResultBuilder result; + private final RestGraphDatabase restGraphDatabase; + + + @Override + public ConvertedResult to(Class type) { + return result.to(type); + } + + @Override + public ConvertedResult to(Class type, ResultConverter converter) { + return result.to(type,converter); + } + + @Override + public void handle(Handler handler) { + result.handle(handler); + } + + @Override + public Iterator iterator() { + return result.iterator(); + } + + public RestQueryResult(Object result, RestGraphDatabase restGraphDatabase, ResultConverter resultConverter) { + this.restGraphDatabase = restGraphDatabase; + final Iterable convertedResult = convertRestResult(result); + this.result=new QueryResultBuilder(convertedResult, resultConverter); + } + + private Iterable convertRestResult(Object result) { + final RestEntityExtractor restEntityExtractor = new RestEntityExtractor(restGraphDatabase); + if (result instanceof Map) { + Map mapResult= (Map) result; + if (RestResultException.isExceptionResult(mapResult)) { + throw new RestResultException(mapResult); + } + if (isTableResult(mapResult)) { + return (Iterable) new RestTableResultExtractor(restEntityExtractor).extract(mapResult); + } + } + if (result instanceof Iterable) { + return new IterableWrapper((Iterable)result) { + @Override + protected T underlyingObjectToObject(Object value) { + return (T) restEntityExtractor.convertFromRepresentation(value); + } + }; + } + return Collections.singletonList((T) restEntityExtractor.convertFromRepresentation(result)); + } + + public static boolean isTableResult(Map mapResult) { + return mapResult.containsKey("columns") && mapResult.containsKey("data"); + } + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestResultException.java b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestResultException.java new file mode 100644 index 000000000..1c4a24cb1 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestResultException.java @@ -0,0 +1,47 @@ +/** + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.neo4j.rest; + +import java.util.List; +import java.util.Map; + +/** + * @author mh + * @since 25.07.11 + */ +public class RestResultException extends RuntimeException { + public RestResultException(Map result) { + super(format(result)); + } + + private static String format(Map result) { + StringBuilder sb = new StringBuilder(); + 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"); + } + } + return sb.toString(); + } + + public static boolean isExceptionResult(Map result) { + return result.containsKey("exception") && result.containsKey("message"); + } +} diff --git a/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestTableResultExtractor.java b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestTableResultExtractor.java new file mode 100644 index 000000000..bd3da3931 --- /dev/null +++ b/spring-data-neo4j-rest/src/main/java/org/springframework/data/neo4j/rest/RestTableResultExtractor.java @@ -0,0 +1,59 @@ +/** + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.neo4j.rest; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author mh + * @since 25.07.11 + */ +public class RestTableResultExtractor { + + private final RestEntityExtractor restEntityExtractor; + + public RestTableResultExtractor(RestEntityExtractor restEntityExtractor) { + this.restEntityExtractor = restEntityExtractor; + } + + public List> extract(Map restResult) { + List columns = (List) restResult.get("columns"); + return extractData(restResult, columns); + } + + private List> extractData(Map restResult, List columns) { + List> rows = (List>) restResult.get("data"); + List> result = new ArrayList>(rows.size()); + for (List row : rows) { + result.add(mapRow(columns, row)); + } + return result; + } + + private Map mapRow(List columns, List row) { + int columnCount = columns.size(); + Map newRow = new HashMap(columnCount); + for (int i = 0; i < columnCount; i++) { + final Object value = row.get(i); + newRow.put(columns.get(i), restEntityExtractor.convertFromRepresentation(value)); + } + return newRow; + } +} diff --git a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestQueryEngineTest.java b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestQueryEngineTest.java index d39b179b5..771a09131 100644 --- a/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestQueryEngineTest.java +++ b/spring-data-neo4j-rest/src/test/java/org/springframework/data/neo4j/rest/support/RestQueryEngineTest.java @@ -22,7 +22,7 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.neo4j.core.GraphDatabase; import org.springframework.data.neo4j.rest.RestGraphDatabase; -import org.springframework.data.neo4j.support.query.QueryOperationsTest; +import org.springframework.data.neo4j.support.query.QueryEngineTest; import org.springframework.test.context.CleanContextCacheTestExecutionListener; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; @@ -39,7 +39,7 @@ import org.springframework.test.context.transaction.TransactionalTestExecutionLi @ContextConfiguration(locations = {"classpath:org/springframework/data/neo4j/support/Neo4jGraphPersistenceTest-context.xml", "classpath:RestTest-context.xml"}) @TestExecutionListeners({CleanContextCacheTestExecutionListener.class, DependencyInjectionTestExecutionListener.class, TransactionalTestExecutionListener.class}) -public class RestQueryEngineTest extends QueryOperationsTest { +public class RestQueryEngineTest extends QueryEngineTest { @Autowired RestGraphDatabase restGraphDatabase; diff --git a/spring-data-neo4j/pom.xml b/spring-data-neo4j/pom.xml index b3a4a18ab..e6119c143 100644 --- a/spring-data-neo4j/pom.xml +++ b/spring-data-neo4j/pom.xml @@ -129,9 +129,26 @@ org.neo4j server-api - provided + optional + + com.tinkerpop.blueprints + blueprints-core + optional + + + com.tinkerpop.blueprints + blueprints-neo4j-graph + optional + + + + com.tinkerpop + gremlin + optional + + commons-configuration commons-configuration diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/annotation/Query.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/annotation/Query.java index f9b5793e6..f7474592e 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/annotation/Query.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/annotation/Query.java @@ -36,7 +36,7 @@ import java.lang.annotation.Target; @Target({ElementType.FIELD,ElementType.METHOD}) public @interface Query { /** - * @return Query to be executed %d will be replaced by the node-id of the current entity other placeholders by the given params + * @return Query to be executed %start will be replaced by the node-id of the current entity other placeholders (%name) by the given named params */ String value() default ""; @@ -46,7 +46,9 @@ public @interface Query { Class elementClass() default Object.class; /** - * @return parameters that are replaced in the to the @see query-string + * @return tuple list of parameters that are replaced in the to the @see query-string {"name", value} */ String[] params() default {}; + + QueryType type() default QueryType.Cypher; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/annotation/QueryType.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/annotation/QueryType.java new file mode 100644 index 000000000..ac702c790 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/annotation/QueryType.java @@ -0,0 +1,25 @@ +/** + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.neo4j.annotation; + +/** + * @author mh + * @since 25.07.11 + */ +public enum QueryType { + Cypher, Gremlin +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/conversion/QueryResultBuilder.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/conversion/QueryResultBuilder.java index 4a81fe2ac..5e1e932b1 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/conversion/QueryResultBuilder.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/conversion/QueryResultBuilder.java @@ -21,6 +21,7 @@ import org.neo4j.helpers.collection.ClosableIterable; import org.neo4j.helpers.collection.IteratorWrapper; import java.util.Iterator; +import java.util.Map; /** * @author mh @@ -42,6 +43,14 @@ public class QueryResultBuilder implements QueryResult { this.defaultConverter = defaultConverter; } + public static String replaceParams(String statement, Map params) { + if (params==null || params.isEmpty()) return statement; + for (Map.Entry param : params.entrySet()) { + statement = statement.replaceAll("%"+param.getKey()+"\\b",""+param.getValue()); + } + return statement; + } + @Override public ConvertedResult to(Class type) { return this.to(type, defaultConverter); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/GraphDatabase.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/GraphDatabase.java index f27c0dcd5..4b4afb8f0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/GraphDatabase.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/GraphDatabase.java @@ -23,7 +23,7 @@ import org.neo4j.graphdb.RelationshipType; import org.neo4j.graphdb.index.Index; import org.neo4j.graphdb.traversal.TraversalDescription; import org.springframework.core.convert.ConversionService; -import org.springframework.data.neo4j.support.query.CypherQueryEngine; +import org.springframework.data.neo4j.annotation.QueryType; import org.springframework.data.neo4j.support.query.QueryEngine; import java.util.Map; @@ -99,7 +99,7 @@ public interface GraphDatabase { */ TraversalDescription createTraversalDescription(); - QueryEngine queryEngineFor(CypherQueryEngine.Type type); + QueryEngine queryEngineFor(QueryType type); void setConversionService(ConversionService conversionService); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeBacked.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeBacked.java index e9a64efb1..bde63c6f8 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeBacked.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/NodeBacked.java @@ -19,6 +19,10 @@ package org.springframework.data.neo4j.core; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.traversal.TraversalDescription; +import org.springframework.data.neo4j.support.node.Neo4jNodeBacking; +import org.springframework.data.neo4j.support.query.CypherQueryExecutor; + +import java.util.Map; /** * Interface introduced to objects annotated with @NodeEntity by the {@link org.springframework.data.neo4j.support.node.Neo4jNodeBacking} aspect. @@ -125,6 +129,11 @@ public interface NodeBacked extends GraphBacked { R getRelationshipTo(NodeBacked target, Class relationshipClass, String type); + Iterable findAllByQuery(final String query, final Class targetType,Map params); + + Iterable> findAllByQuery(final String query,Map params); + + T findByQuery(final String query, final Class targetType,Map params); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/QueryFieldAccessorFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/QueryFieldAccessorFactory.java index 14058ed62..b739fd971 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/QueryFieldAccessorFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/QueryFieldAccessorFactory.java @@ -22,6 +22,7 @@ import org.springframework.data.neo4j.core.NodeBacked; import org.springframework.data.neo4j.support.GenericTypeExtractor; import java.lang.reflect.Field; +import java.util.HashMap; import java.util.Map; import static org.springframework.data.neo4j.support.DoReturn.doReturn; @@ -48,13 +49,16 @@ public class QueryFieldAccessorFactory implements FieldAccessorFactory target; - protected String[] params; + protected String[] annotationParams; private boolean iterableResult; public QueryFieldAccessor(final Field field) { this.field = field; final Query query = field.getAnnotation(Query.class); - this.params = query.params(); + this.annotationParams = query.params(); + if ((this.annotationParams.length % 2) != 0) { + throw new IllegalArgumentException("Number of parameters has to be even to construct a parameter map"); + } this.query = query.value(); this.iterableResult = Iterable.class.isAssignableFrom(field.getType()); this.target = resolveTarget(query,field); @@ -77,23 +81,23 @@ public class QueryFieldAccessorFactory implements FieldAccessorFactory params) { + if (!iterableResult) return nodeBacked.findByQuery(queryString,this.target,params); + if (Map.class.isAssignableFrom(target)) return nodeBacked.findAllByQuery(queryString,params); + return nodeBacked.findAllByQuery(queryString, this.target,params); } - private Object[] createPlaceholderParams(NodeBacked nodeBacked) { - if (params.length==0) return new Object[] {nodeBacked.getNodeId()}; - - final Object[] parameters = new Object[1 + this.params.length]; - parameters[0]=nodeBacked.getNodeId(); - System.arraycopy(this.params,0,parameters,1,this.params.length); - return parameters; + private Map createPlaceholderParams(NodeBacked nodeBacked) { + Map params=new HashMap(); + params.put("start",nodeBacked.getNodeId()); + if (annotationParams.length==0) return params; + for (int i = 0; i < annotationParams.length; i+=2) { + params.put(annotationParams[i],annotationParams[i+1]); + } + return params; } } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/TraversalFieldAccessorFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/TraversalFieldAccessorFactory.java index 668c5578d..a0bcc39ba 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/TraversalFieldAccessorFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/TraversalFieldAccessorFactory.java @@ -19,9 +19,11 @@ package org.springframework.data.neo4j.fieldaccess; import org.neo4j.graphdb.traversal.TraversalDescription; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.neo4j.annotation.GraphTraversal; +import org.springframework.data.neo4j.annotation.Query; import org.springframework.data.neo4j.core.FieldTraversalDescriptionBuilder; import org.springframework.data.neo4j.core.NodeBacked; import org.springframework.data.neo4j.repository.DirectGraphRepositoryFactory; +import org.springframework.data.neo4j.support.GenericTypeExtractor; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -56,11 +58,19 @@ public class TraversalFieldAccessorFactory implements FieldAccessorFactory resolveTarget(GraphTraversal graphTraversal, Field field) { + if (!graphTraversal.elementClass().equals(Object.class)) return graphTraversal.elementClass(); + final Class result = GenericTypeExtractor.resolveFieldType(field); + if (!NodeBacked.class.isAssignableFrom(result)) throw new IllegalArgumentException("The target result type of the traversal is no node entity: "+field); + return (Class) result; + } + + @Override public boolean isWriteable(NodeBacked nodeBacked) { return false; diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/GraphRepositoryFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/GraphRepositoryFactory.java index 3ef2d3fc4..71e298436 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/GraphRepositoryFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/GraphRepositoryFactory.java @@ -20,14 +20,17 @@ import org.neo4j.helpers.collection.IteratorUtil; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.neo4j.annotation.Query; import org.springframework.data.neo4j.annotation.NodeEntity; +import org.springframework.data.neo4j.annotation.Query; +import org.springframework.data.neo4j.annotation.QueryType; import org.springframework.data.neo4j.annotation.RelationshipEntity; import org.springframework.data.neo4j.core.NodeBacked; import org.springframework.data.neo4j.core.RelationshipBacked; import org.springframework.data.neo4j.support.GenericTypeExtractor; import org.springframework.data.neo4j.support.GraphDatabaseContext; -import org.springframework.data.neo4j.support.query.QueryExecutor; +import org.springframework.data.neo4j.support.conversion.EntityResultConverter; +import org.springframework.data.neo4j.support.query.CypherQueryExecutor; +import org.springframework.data.neo4j.support.query.GremlinQueryEngine; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; @@ -38,10 +41,13 @@ import org.springframework.util.Assert; import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import static org.springframework.core.annotation.AnnotationUtils.findAnnotation; +import static org.springframework.data.neo4j.annotation.QueryType.Cypher; +import static org.springframework.data.neo4j.annotation.QueryType.Gremlin; /** * @author mh @@ -109,11 +115,7 @@ public class GraphRepositoryFactory extends RepositoryFactorySupport { @Override public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repositoryMetadata, NamedQueries namedQueries) { final GraphQueryMethod queryMethod = new GraphQueryMethod(method, repositoryMetadata,namedQueries); - - if (queryMethod.isValid()) { - return new GraphRepositoryQuery(queryMethod, repositoryMetadata, graphDatabaseContext); - } - return null; + return queryMethod.createQuery(repositoryMetadata, GraphRepositoryFactory.this.graphDatabaseContext); } }; } @@ -150,19 +152,28 @@ public class GraphRepositoryFactory extends RepositoryFactorySupport { private String prepareQuery(Object[] args) { final Parameters parameters = getParameters(); - Object[] resolvedParameters = resolveParameters(args,parameters.getBindableParameters()); - String baseQuery = String.format(query, (Object[]) resolvedParameters); + String queryString = this.query; if (parameters.hasSortParameter()) { - baseQuery = addSorting(baseQuery, (Sort) args[parameters.getSortIndex()]); + queryString = addSorting(queryString, (Sort) args[parameters.getSortIndex()]); } if (parameters.hasPageableParameter()) { final Pageable pageable = getPageable(args); if (pageable!=null) { - baseQuery = addSorting(baseQuery, pageable.getSort()); - baseQuery = addPaging(baseQuery, pageable); + queryString = addSorting(queryString, pageable.getSort()); + queryString = addPaging(queryString, pageable); } } - return baseQuery; + return queryString; + } + + + private Map resolveParams(Object[] parameters) { + Map params=new HashMap(); + for (Parameter parameter : getParameters().getBindableParameters()) { + final Object value = parameters[parameter.getIndex()]; + params.put(parameter.getName(),resolveParameter(value)); + } + return params; } private Pageable getPageable(Object[] args) { @@ -191,17 +202,6 @@ public class GraphRepositoryFactory extends RepositoryFactorySupport { return result; } - private Object[] resolveParameters(Object[] parameters, Parameters bindableParameters) { - final int paramCount = bindableParameters.getNumberOfParameters(); - final Object[] result = new Object[paramCount]; - for (int i = 0; i < paramCount; i++) { - final Parameter parameter = bindableParameters.getParameter(i); - final Object value = parameters[parameter.getIndex()]; - result[i] = resolveParameter(value); - } - return result; - } - private Object resolveParameter(Object parameter) { if (parameter instanceof NodeBacked) { return ((NodeBacked)parameter).getNodeId(); @@ -232,57 +232,111 @@ public class GraphRepositoryFactory extends RepositoryFactorySupport { public boolean hasAnnotation() { return queryAnnotation!=null; } + + private boolean isIterableResult() { + return Iterable.class.isAssignableFrom(getReturnType()); + } + + private RepositoryQuery createQuery(RepositoryMetadata repositoryMetadata, final GraphDatabaseContext context) { + if (!isValid()) return null; + if (queryAnnotation == null) { + return new CypherGraphRepositoryQuery(this, repositoryMetadata, context); // cypher is default for named queries + } + switch (queryAnnotation.type()) { + case Cypher: + return new CypherGraphRepositoryQuery(this, repositoryMetadata, context); + case Gremlin: + return new GremlinGraphRepositoryQuery(this, repositoryMetadata, context); + default: + throw new IllegalStateException("@Query Annotation has to be configured as Cypher or Gremlin Query"); + } + } } - private static class GraphRepositoryQuery implements RepositoryQuery { - private QueryExecutor queryExecutor; + + private static class CypherGraphRepositoryQuery extends GraphRepositoryQuery { + + private CypherQueryExecutor queryExecutor; + + public CypherGraphRepositoryQuery(GraphQueryMethod queryMethod, RepositoryMetadata metadata, final GraphDatabaseContext graphDatabaseContext) { + super(queryMethod, metadata, graphDatabaseContext); + queryExecutor = new CypherQueryExecutor(graphDatabaseContext); + } + + protected Object dispatchQuery(String queryString, Map params, Pageable pageable) { + GraphQueryMethod queryMethod = getQueryMethod(); + final Class compoundType = queryMethod.getCompoundType(); + final QueryMethod.Type queryResultType = queryMethod.getType(); + if (queryResultType== QueryMethod.Type.PAGING) { + return queryPaged(queryString,params,pageable); + } + if (queryMethod.isIterableResult()) { + if (compoundType.isAssignableFrom(Map.class)) return queryExecutor.queryForList(queryString,params); + return queryExecutor.query(queryString, queryMethod.getCompoundType(),params); + } + return queryExecutor.queryForObject(queryString, queryMethod.getReturnType(),params); + } + private Object queryPaged(String queryString, Map params, Pageable pageable) { + final Iterable result = queryExecutor.query(queryString, getQueryMethod().getCompoundType(),params); + return createPage(result, pageable); + } + } + + private static class GremlinGraphRepositoryQuery extends GraphRepositoryQuery { + + private GremlinQueryEngine queryExecutor; + + public GremlinGraphRepositoryQuery(GraphQueryMethod queryMethod, RepositoryMetadata metadata, final GraphDatabaseContext graphDatabaseContext) { + super(queryMethod, metadata, graphDatabaseContext); + queryExecutor = new GremlinQueryEngine(graphDatabaseContext.getGraphDatabaseService(), new EntityResultConverter(graphDatabaseContext)); + } + + protected Object dispatchQuery(String queryString, Map params, Pageable pageable) { + GraphQueryMethod queryMethod = getQueryMethod(); + final QueryMethod.Type queryResultType = queryMethod.getType(); + if (queryResultType== QueryMethod.Type.PAGING) { + return queryPaged(queryString,params,pageable); + } + if (queryMethod.isIterableResult()) { + return queryExecutor.query(queryString,params).to(queryMethod.getCompoundType()); + } + return queryExecutor.query(queryString, params).to(queryMethod.getReturnType()).single(); + } + + private Object queryPaged(String queryString, Map params, Pageable pageable) { + final Iterable result = queryExecutor.query(queryString, params).to(getQueryMethod().getCompoundType()); + return createPage(result, pageable); + } + + } + + private static abstract class GraphRepositoryQuery implements RepositoryQuery { private final GraphQueryMethod queryMethod; - private final RepositoryMetadata metadata; - private boolean iterableResult; - private Class compoundType; public GraphRepositoryQuery(GraphQueryMethod queryMethod, RepositoryMetadata metadata, final GraphDatabaseContext graphDatabaseContext) { - queryExecutor = new QueryExecutor(graphDatabaseContext); this.queryMethod = queryMethod; - this.metadata = metadata; - this.iterableResult = Iterable.class.isAssignableFrom(queryMethod.getReturnType()); - this.compoundType = queryMethod.getCompoundType(); } @Override public Object execute(Object[] parameters) { + Map params = queryMethod.resolveParams(parameters); final String queryString = queryMethod.prepareQuery(parameters); - return dispatchQuery(queryString,queryMethod.getPageable(parameters)); + return dispatchQuery(queryString,params,queryMethod.getPageable(parameters)); } - private Object dispatchQuery(String queryString, Pageable pageable) { - final QueryMethod.Type queryResultType = queryMethod.getType(); - if (queryResultType== QueryMethod.Type.PAGING) { - return queryPaged(queryString,pageable); - } - if (iterableResult) { - if (compoundType.isAssignableFrom(Map.class)) return queryExecutor.queryForList(queryString); - return queryExecutor.query(queryString, queryMethod.getCompoundType()); - } - return queryExecutor.queryForObject(queryString, queryMethod.getReturnType()); - } - - private Object queryPaged(String queryString, Pageable pageable) { - final Iterable result = queryExecutor.query(queryString, queryMethod.getCompoundType()); - return createPage(result, pageable); - } - - @SuppressWarnings({"unchecked"}) - private Object createPage(Iterable result, Pageable pageable) { - final List resultList = IteratorUtil.addToCollection(result, new ArrayList()); - if (pageable==null) return new PageImpl(resultList); - final int currentTotal = pageable.getOffset() + pageable.getPageSize(); - return new PageImpl(resultList, pageable, currentTotal); - } + protected abstract Object dispatchQuery(String queryString, Map params, Pageable pageable); @Override public GraphQueryMethod getQueryMethod() { return queryMethod; } + + @SuppressWarnings({"unchecked"}) + protected Object createPage(Iterable result, Pageable pageable) { + final List resultList = IteratorUtil.addToCollection(result, new ArrayList()); + if (pageable==null) return new PageImpl(resultList); + final int currentTotal = pageable.getOffset() + pageable.getPageSize(); + return new PageImpl(resultList, pageable, currentTotal); + } } } \ No newline at end of file diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/DelegatingGraphDatabase.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/DelegatingGraphDatabase.java index 0a5b4536c..28d3ad63d 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/DelegatingGraphDatabase.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/DelegatingGraphDatabase.java @@ -23,9 +23,11 @@ import org.neo4j.graphdb.traversal.TraversalDescription; import org.neo4j.index.impl.lucene.LuceneIndexImplementation; import org.neo4j.kernel.Traversal; import org.springframework.core.convert.ConversionService; +import org.springframework.data.neo4j.annotation.QueryType; import org.springframework.data.neo4j.core.GraphDatabase; import org.springframework.data.neo4j.support.query.ConversionServiceQueryResultConverter; import org.springframework.data.neo4j.support.query.CypherQueryEngine; +import org.springframework.data.neo4j.support.query.GremlinQueryEngine; import org.springframework.data.neo4j.support.query.QueryEngine; import java.util.Map; @@ -125,11 +127,12 @@ public class DelegatingGraphDatabase implements GraphDatabase { return Traversal.description(); } - @Override - public QueryEngine queryEngineFor(QueryEngine.Type type) { - if (type == QueryEngine.Type.Cypher) - return new CypherQueryEngine(delegate, createResultConverter()); - throw new IllegalArgumentException("Could not resolve query engine for "+type); + public QueryEngine queryEngineFor(QueryType type) { + switch (type) { + case Cypher: return (QueryEngine)new CypherQueryEngine(delegate, createResultConverter()); + case Gremlin: return (QueryEngine) new GremlinQueryEngine(delegate); + } + throw new IllegalArgumentException("Unknown Query Engine Type "+type); } private ConversionServiceQueryResultConverter createResultConverter() { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/GraphDatabaseContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/GraphDatabaseContext.java index 5c4e4129b..90110cfde 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/GraphDatabaseContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/GraphDatabaseContext.java @@ -73,7 +73,7 @@ public class GraphDatabaseContext { Map config = fullText ? LuceneIndexImplementation.FULLTEXT_CONFIG : null; if (NodeBacked.class.isAssignableFrom(type)) return (Index) getIndexManager().forNodes(indexName, config); if (RelationshipBacked.class.isAssignableFrom(type)) return (Index) getIndexManager().forRelationships(indexName, config); - throw new IllegalArgumentException("Wrong index type supplied: " + type); + throw new IllegalArgumentException("Wrong index type supplied: " + type+" expected Node- or Relationship-Entity"); } /** @@ -240,8 +240,6 @@ public class GraphDatabaseContext { return graphDatabaseService.getRelationshipById(id); } - - public GraphDatabaseService getGraphDatabaseService() { return graphDatabaseService; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/node/Neo4jNodeBacking.aj b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/node/Neo4jNodeBacking.aj index b126cbfad..94f3a35c7 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/node/Neo4jNodeBacking.aj +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/node/Neo4jNodeBacking.aj @@ -31,6 +31,7 @@ import org.springframework.data.neo4j.annotation.RelationshipEntity; import org.springframework.data.neo4j.annotation.RelatedTo; import org.springframework.data.neo4j.annotation.GraphProperty; import org.springframework.data.neo4j.annotation.GraphId; +import org.springframework.data.neo4j.annotation.Query; import org.springframework.data.neo4j.annotation.RelatedToVia; import org.springframework.data.neo4j.annotation.GraphTraversal; @@ -42,7 +43,7 @@ import org.springframework.data.neo4j.core.EntityState; import org.springframework.data.neo4j.support.GraphDatabaseContext; import org.springframework.data.neo4j.support.path.EntityPathPathIterableWrapper; -import org.springframework.data.neo4j.support.query.QueryExecutor; +import org.springframework.data.neo4j.support.query.CypherQueryExecutor; import javax.persistence.Transient; import javax.persistence.Entity; @@ -71,6 +72,7 @@ public aspect Neo4jNodeBacking { // extends AbstractTypeAnnotatingMixinFields(traverser, targetType, Neo4jNodeBacking.aspectOf().graphDatabaseContext); } - public Iterable NodeBacked.findAllByQuery(final String query, final Class targetType) { - final QueryExecutor executor = new QueryExecutor(Neo4jNodeBacking.aspectOf().graphDatabaseContext); - return executor.query(query, targetType); + + public Iterable NodeBacked.findAllByQuery(final String query, final Class targetType, Map params) { + final CypherQueryExecutor executor = new CypherQueryExecutor(Neo4jNodeBacking.aspectOf().graphDatabaseContext); + return executor.query(query, targetType,params); } - public Iterable> NodeBacked.findAllByQuery(final String query) { - final QueryExecutor executor = new QueryExecutor(Neo4jNodeBacking.aspectOf().graphDatabaseContext); - return executor.queryForList(query); + public Iterable> NodeBacked.findAllByQuery(final String query,Map params) { + final CypherQueryExecutor executor = new CypherQueryExecutor(Neo4jNodeBacking.aspectOf().graphDatabaseContext); + return executor.queryForList(query,params); } - public T NodeBacked.findByQuery(final String query, final Class targetType) { - final QueryExecutor executor = new QueryExecutor(Neo4jNodeBacking.aspectOf().graphDatabaseContext); - return executor.queryForObject(query, targetType); + public T NodeBacked.findByQuery(final String query, final Class targetType,Map params) { + final CypherQueryExecutor executor = new CypherQueryExecutor(Neo4jNodeBacking.aspectOf().graphDatabaseContext); + return executor.queryForObject(query, targetType,params); } public Iterable> NodeBacked.findAllPathsByTraversal(TraversalDescription traversalDescription) { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/CypherQueryEngine.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/CypherQueryEngine.java index 95ccc4766..b62f88772 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/CypherQueryEngine.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/CypherQueryEngine.java @@ -29,11 +29,10 @@ import org.springframework.data.neo4j.conversion.ResultConverter; import java.util.Map; -public class CypherQueryEngine implements QueryEngine, QueryOperations { +public class CypherQueryEngine implements QueryEngine> { final ExecutionEngine executionEngine; private ResultConverter resultConverter; - private final DefaultQueryOperations queryOperations; public CypherQueryEngine(GraphDatabaseService graphDatabaseService) { this(graphDatabaseService, new DefaultConverter()); @@ -43,34 +42,19 @@ public class CypherQueryEngine implements QueryEngine, QueryOperations { public CypherQueryEngine(GraphDatabaseService graphDatabaseService, ResultConverter resultConverter) { this.resultConverter = resultConverter != null ? resultConverter : new DefaultConverter(); this.executionEngine = new ExecutionEngine(graphDatabaseService); - this.queryOperations = new DefaultQueryOperations(this); } @Override - public QueryResult> query(String statement) { + public QueryResult> query(String statement, Map params) { try { - ExecutionResult result = parseAndExecuteQuery(statement); + String parametrizedQuery = QueryResultBuilder.replaceParams(statement,params); + ExecutionResult result = parseAndExecuteQuery(parametrizedQuery); return new QueryResultBuilder>(result,resultConverter); } catch (Exception e) { throw new InvalidDataAccessResourceUsageException("Error executing statement " + statement, e); } } - @Override - public Iterable> queryForList(String statement) { - return queryOperations.queryForList(statement); - } - - @Override - public Iterable query(String statement, Class type) { - return queryOperations.query(statement, type); - } - - @Override - public T queryForObject(String statement, Class type) { - return queryOperations.queryForObject(statement, type); - } - private ExecutionResult parseAndExecuteQuery(String statement) { try { CypherParser parser = new CypherParser(); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/QueryExecutor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/CypherQueryExecutor.java similarity index 61% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/QueryExecutor.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/CypherQueryExecutor.java index a5df1bafe..9a62c8bcf 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/QueryExecutor.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/CypherQueryExecutor.java @@ -19,6 +19,7 @@ package org.springframework.data.neo4j.support.query; import org.springframework.data.neo4j.support.GraphDatabaseContext; import org.springframework.data.neo4j.support.conversion.EntityResultConverter; +import java.util.Collections; import java.util.Map; /** @@ -26,24 +27,27 @@ import java.util.Map; * @since 10.06.11 * todo limits */ -public class QueryExecutor implements QueryOperations { - private final QueryEngine queryEngine; +public class CypherQueryExecutor implements QueryOperations> { + private final CypherQueryEngine queryEngine; - public QueryExecutor(GraphDatabaseContext ctx) { + public CypherQueryExecutor(GraphDatabaseContext ctx) { EntityResultConverter converter = new EntityResultConverter(ctx); queryEngine = new CypherQueryEngine(ctx.getGraphDatabaseService(), converter); } - public Iterable> queryForList(String statement) { - return queryEngine.query(statement); + public Iterable> queryForList(String statement, Map params) { + return queryEngine.query(statement,mergeParams(params)); } - public Iterable query(String statement, Class type) { - return queryEngine.query(statement).to(type); + public Iterable query(String statement, Class type, Map params) { + return queryEngine.query(statement,mergeParams(params)).to(type); } - public T queryForObject(String statement, Class type) { - return queryEngine.query(statement).to(type).single(); + public T queryForObject(String statement, Class type, Map params) { + return (T) queryEngine.query(statement,mergeParams(params)).to(type).single(); + } + private Map mergeParams(Map params) { + if (params==null) return Collections.emptyMap(); + return params; } - } \ No newline at end of file diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/DefaultQueryOperations.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/DefaultQueryOperations.java deleted file mode 100644 index 43af09ac6..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/DefaultQueryOperations.java +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright 2011 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.data.neo4j.support.query; - -import java.util.Map; - -/** - * @author mh - * @since 28.06.11 - */ -public class DefaultQueryOperations implements QueryOperations { - private QueryEngine queryEngine; - - public DefaultQueryOperations(QueryEngine queryEngine) { - this.queryEngine = queryEngine; - } - - public Iterable> queryForList(String statement) { - return queryEngine.query(statement); - } - - public Iterable query(String statement, Class type) { - return queryEngine.query(statement).to(type); - } - - public T queryForObject(String statement, Class type) { - return queryEngine.query(statement).to(type).single(); - } - -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/GremlinExecutor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/GremlinExecutor.java new file mode 100644 index 000000000..2b3a0718f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/GremlinExecutor.java @@ -0,0 +1,108 @@ +/** + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.neo4j.support.query; + +import com.tinkerpop.blueprints.pgm.Edge; +import com.tinkerpop.blueprints.pgm.Vertex; +import com.tinkerpop.blueprints.pgm.impls.neo4j.Neo4jEdge; +import com.tinkerpop.blueprints.pgm.impls.neo4j.Neo4jGraph; +import com.tinkerpop.blueprints.pgm.impls.neo4j.Neo4jVertex; +import com.tinkerpop.gremlin.pipes.util.Table; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.helpers.collection.IterableWrapper; + +import javax.script.*; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +@SuppressWarnings("ALL") +public class GremlinExecutor { + + public static final int REFRESH_ENGINE_COUNT = 10000; + private final String g = "g"; + private volatile ScriptEngine engine; + + private ScriptEngine createScriptEngine() { + return new ScriptEngineManager().getEngineByName("gremlin"); + } + + private static final AtomicInteger executionCount = new AtomicInteger(); + private final GraphDatabaseService graphDatabaseService; + + public GremlinExecutor(GraphDatabaseService graphDatabaseService) { + this.graphDatabaseService = graphDatabaseService; + } + + public Iterable query(String statement, Map params) { + try { + final Bindings bindings = createBindings(params); + final ScriptEngine engine = engine(); + final Object result = engine.eval(statement, bindings); + return getRepresentation(result); + } catch (final ScriptException e) { + throw new RuntimeException("Error executing statement " + statement, e); + } + } + + private Bindings createBindings(Map params) { + final Bindings bindings = new SimpleBindings(); + bindings.put(g, new Neo4jGraph(graphDatabaseService)); + if (params==null) return bindings; + for (Map.Entry entry : params.entrySet()) { + bindings.put(entry.getKey(),entry.getValue()); + } + return bindings; + } + + private ScriptEngine engine() { + if (engine == null || executionCount.incrementAndGet() > REFRESH_ENGINE_COUNT) { + executionCount.set(0); + this.engine = new ScriptEngineManager().getEngineByName("gremlin"); + } + return this.engine; + } + + + public static Iterable getRepresentation(final Object result) { + if (result instanceof Iterable) { + if (result instanceof Table) { + Table table = (Table) result; + } + return new IterableWrapper((Iterable) result) { + @Override + protected Object underlyingObjectToObject(Object object) { + return getSingleResult(object); + } + }; + } else { + return Collections.singleton(getSingleResult(result)); + } + } + + private static Object getSingleResult(Object result) { + if (result instanceof Vertex) { + return ((Neo4jVertex) result).getRawVertex(); + } else if (result instanceof Edge) { + return ((Neo4jEdge) result).getRawEdge(); + } else if (result instanceof Neo4jGraph) { + return ((Neo4jGraph) result).getRawGraph(); + } else { + return result; + } + } +} \ No newline at end of file diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/GremlinQueryEngine.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/GremlinQueryEngine.java new file mode 100644 index 000000000..e8ec9b56c --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/GremlinQueryEngine.java @@ -0,0 +1,52 @@ +/** + * Copyright 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.neo4j.support.query; + +import org.neo4j.graphdb.GraphDatabaseService; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.data.neo4j.conversion.DefaultConverter; +import org.springframework.data.neo4j.conversion.QueryResult; +import org.springframework.data.neo4j.conversion.QueryResultBuilder; +import org.springframework.data.neo4j.conversion.ResultConverter; + +import java.util.Map; + +public class GremlinQueryEngine implements QueryEngine { + + private final GremlinExecutor gremlinExecutor; + private final ResultConverter resultConverter; + + public GremlinQueryEngine(GraphDatabaseService graphDatabaseService) { + this(graphDatabaseService, new DefaultConverter()); + } + + + public GremlinQueryEngine(GraphDatabaseService graphDatabaseService, ResultConverter resultConverter) { + this.resultConverter = resultConverter != null ? resultConverter : new DefaultConverter(); + this.gremlinExecutor = new GremlinExecutor(graphDatabaseService); + } + + @Override + public QueryResult query(String statement, Map params) { + try { + Iterable result = gremlinExecutor.query(statement, params); + return new QueryResultBuilder(result,resultConverter); + } catch (Exception e) { + throw new InvalidDataAccessResourceUsageException("Error executing statement " + statement, e); + } + } +} \ No newline at end of file diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/QueryEngine.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/QueryEngine.java index e6e494c9b..c24822581 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/QueryEngine.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/QueryEngine.java @@ -24,8 +24,7 @@ import java.util.Map; * @author mh * @since 22.06.11 */ -public interface QueryEngine { - QueryResult> query(String statement); +public interface QueryEngine { + QueryResult query(String statement, Map params); - public enum Type { Cypher } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/QueryOperations.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/QueryOperations.java index dfa6221e4..69bea6771 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/QueryOperations.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/query/QueryOperations.java @@ -22,10 +22,10 @@ import java.util.Map; * @author mh * @since 28.06.11 */ -public interface QueryOperations { - Iterable> queryForList(String statement); +public interface QueryOperations { + Iterable queryForList(String statement, Map params); - Iterable query(String statement, Class type); + Iterable query(String statement, Class type, Map params); - T queryForObject(String statement, Class type); + T queryForObject(String statement, Class type, Map params); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/template/Neo4jOperations.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/template/Neo4jOperations.java index 1c4697cc3..b16c12af3 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/template/Neo4jOperations.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/template/Neo4jOperations.java @@ -98,15 +98,40 @@ public interface Neo4jOperations { */ T index(String indexName, T element, String field, Object value); + /** + * Converts the Iterable into a QueryResult object for uniform handling. E.g. + * template.convert(node.getRelationships()); + */ QueryResult convert(Iterable iterable); - QueryResult> query(String statement); + /** + * Runs the given cypher statement and packages the result in a QueryResult, simple conversions via the + * registered converter-factories are already executed via this method. + */ + QueryResult> query(String statement,Map params); + /** + * Executes the given Gremlin statement and returns the result packaged as QueryResult as Neo4j types, not + * Gremlin types. Table rows are converted to Map. + */ + QueryResult execute(String statement, Map params); + + /** + * Traverses the graph starting at the given node with the provided traversal description. The Path's of the + * traversal will be packaged into a QueryResult which can be easily converted into Nodes, Relationships or + * Graph-Entities. + */ QueryResult traverse(Node startNode, TraversalDescription traversal); + /** + * The value is looked up in the Neo4j index returning the IndexHits wrapped in a QueryResult to be converted + * into Paths or Entities. + */ QueryResult lookup(String indexName, String field, Object value); - QueryResult lookup(String indexName, Object valueOrQueryObject); - - Node createNode(); + /** + * The query is executed on the index returning the IndexHits wrapped in a QueryResult to be converted + * into Paths or Entities. + */ + QueryResult lookup(String indexName, Object query); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/template/Neo4jTemplate.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/template/Neo4jTemplate.java index 6d246bed1..f568c25e9 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/template/Neo4jTemplate.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/template/Neo4jTemplate.java @@ -18,17 +18,13 @@ package org.springframework.data.neo4j.template; import org.neo4j.graphdb.*; import org.neo4j.graphdb.index.Index; -import org.neo4j.graphdb.index.IndexHits; import org.neo4j.graphdb.traversal.TraversalDescription; -import org.neo4j.helpers.collection.ClosableIterable; -import org.neo4j.helpers.collection.IterableWrapper; import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.neo4j.annotation.QueryType; import org.springframework.data.neo4j.conversion.QueryResult; import org.springframework.data.neo4j.conversion.QueryResultBuilder; import org.springframework.data.neo4j.core.GraphDatabase; -import org.springframework.data.neo4j.support.path.PathMapper; -import org.springframework.data.neo4j.support.query.CypherQueryEngine; import org.springframework.data.neo4j.support.query.QueryEngine; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; @@ -167,7 +163,7 @@ public class Neo4jTemplate implements Neo4jOperations { return new QueryResultBuilder(iterable); } - private QueryEngine queryEngineFor(CypherQueryEngine.Type type) { + private QueryEngine queryEngineFor(QueryType type) { return graphDatabase.queryEngineFor(type); } @@ -182,38 +178,15 @@ public class Neo4jTemplate implements Neo4jOperations { }); } - @Override - public Node createNode() { - return createNode(null); - } - - private static abstract class IndexHitsIterableWrapper extends IterableWrapper implements ClosableIterable { - private final IndexHits indexHits; - private final PathMapper pathMapper; - - public IndexHitsIterableWrapper(IndexHits indexHits, PathMapper pathMapper) { - super(indexHits); - this.indexHits = indexHits; - this.pathMapper = pathMapper; - } - - @Override - protected T underlyingObjectToObject(S node) { - return pathMapper.mapPath(createPath(node)); - } - - protected abstract Path createPath(S element); - - @Override - public void close() { - indexHits.close(); - } - } - - @Override - public QueryResult> query(String statement) { + public QueryResult> query(String statement, Map params) { notNull(statement, "statement"); - return queryEngineFor(QueryEngine.Type.Cypher).query(statement); + return queryEngineFor(QueryType.Cypher).query(statement, params); + } + + @Override + public QueryResult execute(String statement, Map params) { + notNull(statement, "statement"); + return queryEngineFor(QueryType.Gremlin).query(statement, params); } @Override @@ -237,11 +210,11 @@ public class Neo4jTemplate implements Neo4jOperations { } } @Override - public QueryResult lookup(String indexName, Object valueOrQueryObject) { - notNull(valueOrQueryObject, "valueOrQueryObject", indexName, "indexName"); + public QueryResult lookup(String indexName, Object query) { + notNull(query, "valueOrQueryObject", indexName, "indexName"); try { Index index = graphDatabase.getIndex(indexName); - return new QueryResultBuilder(index.query(valueOrQueryObject)); + return new QueryResultBuilder(index.query(query)); } catch (RuntimeException e) { throw translateExceptionIfPossible(e); } diff --git a/spring-data-neo4j/src/main/resources/META-INF/spring.schemas b/spring-data-neo4j/src/main/resources/META-INF/spring.schemas index 0b7c5a9ce..c6cec6413 100644 --- a/spring-data-neo4j/src/main/resources/META-INF/spring.schemas +++ b/spring-data-neo4j/src/main/resources/META-INF/spring.schemas @@ -1 +1 @@ -http\://www.springframework.org/schema/data/graph/datagraph-1.0.xsd=org/springframework/data/graph/neo4j/config/datagraph-1.0.xsd \ No newline at end of file +http\://www.springframework.org/schema/data/graph/datagraph-1.0.xsd=org/springframework/data/neo4j/config/datagraph-1.0.xsd \ No newline at end of file diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/Person.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/Person.java index 338a34826..c5c4ec449 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/Person.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/Person.java @@ -66,16 +66,16 @@ public class Person { @RelatedToVia(type = "knows", elementClass = Friendship.class) private Iterable friendships; - @Query(value = "start person=(%d) match (person)<-[:boss]-(boss) return boss") + @Query(value = "start person=(%start) match (person)<-[:boss]-(boss) return boss") private Person bossByQuery; - @Query(value = "start person=(%d) match (person)<-[:boss]-(boss) return boss.%s",params = "name") + @Query(value = "start person=(%start) match (person)<-[:boss]-(boss) return boss.%property",params = {"property","name"}) private String bossName; - @Query(value = "start person=(%d) match (person)<-[:persons]-(team)-[:persons]->(member) return member",elementClass = Person.class) + @Query(value = "start person=(%start) match (person)<-[:persons]-(team)-[:persons]->(member) return member",elementClass = Person.class) private Iterable otherTeamMembers; - @Query(value = "start person=(%d) match (person)<-[:persons]-(team)-[:persons]->(member) return member.name, member.age") + @Query(value = "start person=(%start) match (person)<-[:persons]-(team)-[:persons]->(member) return member.name, member.age") private Iterable> otherTeamMemberData; public String getBossName() { diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/PersonRepository.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/PersonRepository.java index 19e57094f..8f8cc7e1b 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/PersonRepository.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/PersonRepository.java @@ -20,8 +20,10 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.annotation.Query; +import org.springframework.data.neo4j.annotation.QueryType; import org.springframework.data.neo4j.repository.GraphRepository; import org.springframework.data.neo4j.repository.NamedIndexRepository; +import org.springframework.data.repository.query.Param; import java.util.Map; @@ -31,19 +33,22 @@ import java.util.Map; */ public interface PersonRepository extends GraphRepository, NamedIndexRepository { - @Query("start team=(%d) match (team)-[:persons]->(member) return member") - Iterable findAllTeamMembers(Group team); + @Query("start team=(%team) match (team)-[:persons]->(member) return member") + Iterable findAllTeamMembers(@Param("team") Group team); - @Query("start team=(%d) match (team)-[:persons]->(member) return member.name,member.age") - Iterable> findAllTeamMemberData(Group team); + @Query(value = "g.v(team).out('persons')", type = QueryType.Gremlin) + Iterable findAllTeamMembersGremlin(@Param("team") Group team); - @Query("start person=(%d) match (boss)-[:boss]->(person) return boss") - Person findBoss(Person person); + @Query("start team=(%team) match (team)-[:persons]->(member) return member.name,member.age") + Iterable> findAllTeamMemberData(@Param("team") Group team); - Group findTeam(Person person); + @Query("start person=(%person) match (boss)-[:boss]->(person) return boss") + Person findBoss(@Param("person") Person person); - @Query("start team=(%d) match (team)-[:persons]->(member) return member") - Page findAllTeamMembersPaged(Pageable page, Group team); - @Query("start team=(%d) match (team)-[:persons]->(member) return member") - Iterable findAllTeamMembersSorted(Group team, Sort sort); + Group findTeam(@Param("person") Person person); + + @Query("start team=(%team) match (team)-[:persons]->(member) return member") + Page findAllTeamMembersPaged(@Param("team") Group team, Pageable page); + @Query("start team=(%team) match (team)-[:persons]->(member) return member") + Iterable findAllTeamMembersSorted(@Param("team") Group team, Sort sort); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/GraphRepositoryTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/GraphRepositoryTest.java index 5e7a7c92b..faf7bd6ad 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/GraphRepositoryTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/GraphRepositoryTest.java @@ -75,6 +75,12 @@ public class GraphRepositoryTest { Iterable teamMembers = personRepository.findAllTeamMembers(testTeam.sdg); assertThat(asCollection(teamMembers), hasItems(testTeam.michael, testTeam.david, testTeam.emil)); } + @Test + @Transactional + public void testFindIterableOfPersonWithQueryAnnotationAndGremlin() { + Iterable teamMembers = personRepository.findAllTeamMembersGremlin(testTeam.sdg); + assertThat(asCollection(teamMembers), hasItems(testTeam.michael, testTeam.david, testTeam.emil)); + } @Test @Transactional @@ -93,21 +99,21 @@ public class GraphRepositoryTest { @Transactional public void testFindPaged() { final PageRequest page = new PageRequest(0, 1, Sort.Direction.ASC, "member.name"); - Page teamMemberPage1 = personRepository.findAllTeamMembersPaged(page, testTeam.sdg); + Page teamMemberPage1 = personRepository.findAllTeamMembersPaged(testTeam.sdg,page); assertThat(teamMemberPage1, hasItem(testTeam.david)); } @Test @Transactional public void testFindPagedDescending() { final PageRequest page = new PageRequest(0, 2, Sort.Direction.DESC, "member.name"); - Page teamMemberPage1 = personRepository.findAllTeamMembersPaged(page, testTeam.sdg); + Page teamMemberPage1 = personRepository.findAllTeamMembersPaged(testTeam.sdg,page); assertEquals(asList(testTeam.michael, testTeam.emil), asCollection(teamMemberPage1)); assertThat(teamMemberPage1.isFirstPage(), is(true)); } @Test @Transactional public void testFindPagedNull() { - Page teamMemberPage1 = personRepository.findAllTeamMembersPaged(null, testTeam.sdg); + Page teamMemberPage1 = personRepository.findAllTeamMembersPaged(testTeam.sdg,null); assertEquals(asList(testTeam.michael, testTeam.emil,testTeam.david), asCollection(teamMemberPage1)); assertThat(teamMemberPage1.isFirstPage(), is(true)); assertThat(teamMemberPage1.isLastPage(), is(false)); diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/query/QueryEngineTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/query/QueryEngineTest.java index c502191e5..552ff8e60 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/query/QueryEngineTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/query/QueryEngineTest.java @@ -21,15 +21,15 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.neo4j.graphdb.Node; import org.neo4j.helpers.collection.IteratorUtil; +import org.neo4j.helpers.collection.MapUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.convert.ConversionService; import org.springframework.data.neo4j.Person; import org.springframework.data.neo4j.Personality; +import org.springframework.data.neo4j.annotation.QueryType; +import org.springframework.data.neo4j.conversion.ResultConverter; import org.springframework.data.neo4j.core.GraphDatabase; import org.springframework.data.neo4j.core.NodeBacked; -import org.springframework.data.neo4j.Person; -import org.springframework.data.neo4j.Personality; -import org.springframework.data.neo4j.conversion.ResultConverter; import org.springframework.data.neo4j.support.DelegatingGraphDatabase; import org.springframework.data.neo4j.support.GraphDatabaseContext; import org.springframework.data.neo4j.support.TestTeam; @@ -59,7 +59,7 @@ public class QueryEngineTest { protected ConversionService conversionService; @Autowired private GraphDatabaseContext graphDatabaseContext; - private QueryEngine queryEngine; + private QueryEngine> queryEngine; private TestTeam testTeam; private Person michael; private GraphDatabase graphDatabase; @@ -69,7 +69,7 @@ public class QueryEngineTest { graphDatabase = createGraphDatabase(); testTeam = new TestTeam(); testTeam.createSDGTeam(); - queryEngine = graphDatabase.queryEngineFor(QueryEngine.Type.Cypher); + queryEngine = graphDatabase.queryEngineFor(QueryType.Cypher); michael = testTeam.michael; } @@ -87,31 +87,35 @@ public class QueryEngineTest { @Test @Transactional public void testQueryList() throws Exception { - final String queryString = String.format("start person=(%d,%d) return person.name, person.age", idFor(michael), idFor(testTeam.david)); - final Collection> result = IteratorUtil.asCollection(queryEngine.query(queryString)); + final String queryString = "start person=(%michael,%david) return person.name, person.age"; + final Collection> result = IteratorUtil.asCollection(queryEngine.query(queryString, MapUtil.map("michael",idFor(michael), "david",idFor(testTeam.david)))); assertEquals(asList(testTeam.simpleRowFor(michael,"person"),testTeam.simpleRowFor(testTeam.david,"person")),result); } @Test public void testQueryListOfTypeNode() throws Exception { - final String queryString = String.format("start person=(name_index,name,\"%s\") match (person) <-[:boss]- (boss) return boss", michael.getName()); - final Collection result = IteratorUtil.asCollection(queryEngine.query(queryString).to(Node.class)); + final String queryString = "start person=(name_index,name,\"%name\") match (person) <-[:boss]- (boss) return boss"; + final Collection result = IteratorUtil.asCollection(queryEngine.query(queryString, michaelsName()).to(Node.class)); assertEquals(asList(nodeFor(testTeam.emil)),result); } @Test public void testQueryListOfTypePerson() throws Exception { - final String queryString = String.format("start person=(name_index,name,\"%s\") match (person) <-[:boss]- (boss) return boss", michael.getName()); - final Collection result = IteratorUtil.asCollection(queryEngine.query(queryString).to(Person.class, new EntityResultConverter(graphDatabaseContext))); + final String queryString = "start person=(name_index,name,\"%name\") match (person) <-[:boss]- (boss) return boss"; + final Collection result = IteratorUtil.asCollection(queryEngine.query(queryString, michaelsName()).to(Person.class, new EntityResultConverter(graphDatabaseContext))); assertEquals(asList(testTeam.emil),result); } + private Map michaelsName() { + return MapUtil.map("name", michael.getName()); + } + @Test public void testQuerySingleOfTypePerson() throws Exception { - final String queryString = String.format("start person=(name_index,name,\"%s\") match (person) <-[:boss]- (boss) return boss", michael.getName()); - final Person result = queryEngine.query(queryString).to(Person.class, new EntityResultConverter,Person>(graphDatabaseContext)).single(); + final String queryString = "start person=(name_index,name,\"%name\") match (person) <-[:boss]- (boss) return boss"; + final Person result = queryEngine.query(queryString, michaelsName()).to(Person.class, new EntityResultConverter,Person>(graphDatabaseContext)).single(); assertEquals(testTeam.emil,result); } @@ -119,7 +123,7 @@ public class QueryEngineTest { @Test public void testQueryListWithCustomConverter() throws Exception { final String queryString = String.format("start person=(name_index,name,\"%s\") match (person) <-[:boss]- (boss) return boss", michael.getName()); - final Collection result = IteratorUtil.asCollection(queryEngine.query(queryString).to(String.class, new ResultConverter, String>() { + final Collection result = IteratorUtil.asCollection(queryEngine.query(queryString, michaelsName()).to(String.class, new ResultConverter, String>() { @Override public String convert(Map row, Class target) { return (String) ((Node) row.get("boss")).getProperty("name"); @@ -138,15 +142,15 @@ public class QueryEngineTest { @Test public void testQueryForObjectAsString() throws Exception { - final String queryString = String.format("start person=(name_index,name,\"%s\") match (person) <-[:persons]- (team) return team.name", michael.getName()); - final String result = queryEngine.query(queryString).to(String.class).single(); + final String queryString = "start person=(name_index,name,\"%name\") match (person) <-[:persons]- (team) return team.name"; + final String result = queryEngine.query(queryString, michaelsName()).to(String.class).single(); assertEquals(testTeam.sdg.getName(),result); } @Test public void testQueryForObjectAsEnum() throws Exception { - final String queryString = String.format("start person=(name_index,name,\"%s\") return person.personality", michael.getName()); - final Personality result = queryEngine.query(queryString).to(Personality.class).single(); + final String queryString = "start person=(name_index,name,\"%name\") return person.personality"; + final Personality result = queryEngine.query(queryString, michaelsName()).to(Personality.class).single(); assertEquals(michael.getPersonality(),result); } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/query/QueryExecutorTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/query/QueryExecutorTest.java deleted file mode 100644 index d705ab4f7..000000000 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/query/QueryExecutorTest.java +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Copyright 2011 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.data.neo4j.support.query; - -import org.junit.Before; -import org.junit.Test; -import org.junit.internal.matchers.IsCollectionContaining; -import org.junit.runner.RunWith; -import org.neo4j.helpers.collection.IteratorUtil; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.neo4j.Group; -import org.springframework.data.neo4j.*; -import org.springframework.data.neo4j.Person; -import org.springframework.data.neo4j.Personality; -import org.springframework.data.neo4j.support.GraphDatabaseContext; -import org.springframework.data.neo4j.support.TestTeam; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Collection; -import java.util.Map; - -import static java.util.Arrays.asList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; - -/** - * @author mh - * @since 13.06.11 - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(locations = {"classpath:org/springframework/data/neo4j/support/Neo4jGraphPersistenceTest-context.xml"}) -@Transactional -public class QueryExecutorTest { - @Autowired - GraphDatabaseContext graphDatabaseContext; - private QueryExecutor queryExecutor; - private TestTeam testTeam; - private Person michael; - - @Before - public void setUp() throws Exception { - testTeam = new TestTeam(); - testTeam.createSDGTeam(); - queryExecutor = new QueryExecutor(graphDatabaseContext); - michael = testTeam.michael; - } - - @Test - @Transactional - public void testQueryList() throws Exception { - final String queryString = String.format("start person=(%d,%d) return person.name, person.age", michael.getNodeId(), testTeam.david.getNodeId()); - final Collection> result = IteratorUtil.asCollection(queryExecutor.queryForList(queryString)); - - assertEquals(asList(testTeam.simpleRowFor(michael,"person"),testTeam.simpleRowFor(testTeam.david,"person")),result); - } - - @Test - public void testQueryListOfTypePerson() throws Exception { - final String queryString = String.format("start person=(name_index,name,\"%s\") match (person) <-[:boss]- (boss) return boss", michael.getName()); - final Collection result = IteratorUtil.asCollection(queryExecutor.query(queryString, Person.class)); - - assertEquals(asList(testTeam.emil),result); - - } - - @Test - public void testQueryOtherTeamMembers() throws Exception { - final String queryString = String.format("start person=(%d) match (person)<-[:persons]-(team)-[:persons]->(member) return member", michael.getNodeId()); - System.out.println("testTeam = " + testTeam.sdg.getPersons()); - final Collection result = IteratorUtil.asCollection(queryExecutor.query(queryString, Person.class)); - - assertThat(result, IsCollectionContaining.hasItems(testTeam.david, testTeam.emil)); - - } - - @Test - public void testQueryAllTeamMembersByTeam() throws Exception { - final String queryString = String.format("start team=(Group,name,\"%s\") match (team)-[:persons]->(member) return member", testTeam.sdg.getName()); - final Collection result = IteratorUtil.asCollection(queryExecutor.query(queryString, Person.class)); - - assertThat(result, IsCollectionContaining.hasItems(testTeam.david,testTeam.michael)); - } - - @Test - public void testQueryForObjectAsGroup() throws Exception { - final String queryString = String.format("start person=(name_index,name,\"%s\") match (person) <-[:persons]- (team) return team", michael.getName()); - final Group result = queryExecutor.queryForObject(queryString, Group.class); - - assertEquals(testTeam.sdg,result); - } - @Test - public void testQueryForObjectAsString() throws Exception { - final String queryString = String.format("start person=(name_index,name,\"%s\") match (person) <-[:persons]- (team) return team.name", michael.getName()); - final String result = queryExecutor.queryForObject(queryString, String.class); - - assertEquals(testTeam.sdg.getName(),result); - } - @Test - public void testQueryForObjectAsEnum() throws Exception { - final String queryString = String.format("start person=(name_index,name,\"%s\") return person.personality", michael.getName()); - final Personality result = queryExecutor.queryForObject(queryString, Personality.class); - - assertEquals(michael.getPersonality(),result); - } -} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/query/QueryOperationsTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/query/QueryOperationsTest.java deleted file mode 100644 index 6749775c3..000000000 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/query/QueryOperationsTest.java +++ /dev/null @@ -1,151 +0,0 @@ -/** - * Copyright 2011 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.data.neo4j.support.query; - -import org.junit.Before; -import org.junit.Test; -import org.junit.internal.matchers.IsCollectionContaining; -import org.junit.runner.RunWith; -import org.neo4j.graphdb.Node; -import org.neo4j.helpers.collection.IteratorUtil; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.convert.ConversionService; -import org.springframework.data.neo4j.Person; -import org.springframework.data.neo4j.Personality; -import org.springframework.data.neo4j.core.GraphDatabase; -import org.springframework.data.neo4j.core.NodeBacked; -import org.springframework.data.neo4j.Person; -import org.springframework.data.neo4j.Personality; -import org.springframework.data.neo4j.support.DelegatingGraphDatabase; -import org.springframework.data.neo4j.support.GraphDatabaseContext; -import org.springframework.data.neo4j.support.TestTeam; -import org.springframework.data.neo4j.support.node.Neo4jHelper; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.transaction.BeforeTransaction; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Collection; -import java.util.Map; - -import static java.util.Arrays.asList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; - - -/** - * @author mh - * @since 13.06.11 - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(locations = {"classpath:org/springframework/data/neo4j/support/Neo4jGraphPersistenceTest-context.xml"}) -@Transactional -public class QueryOperationsTest { - @Autowired - protected ConversionService conversionService; - @Autowired - private GraphDatabaseContext graphDatabaseContext; - private QueryOperations queryOperations; - private TestTeam testTeam; - private Person michael; - private GraphDatabase graphDatabase; - - @Before - public void setUp() throws Exception { - graphDatabase = createGraphDatabase(); - testTeam = new TestTeam(); - testTeam.createSDGTeam(); - queryOperations = new DefaultQueryOperations(graphDatabase.queryEngineFor(QueryEngine.Type.Cypher)); - michael = testTeam.michael; - } - - @BeforeTransaction - public void cleanDb() { - Neo4jHelper.cleanDb(graphDatabaseContext); - } - - protected GraphDatabase createGraphDatabase() throws Exception { - final DelegatingGraphDatabase graphDatabase = new DelegatingGraphDatabase(graphDatabaseContext.getGraphDatabaseService()); - graphDatabase.setConversionService(conversionService); - return graphDatabase; - } - - @Test - @Transactional - public void testQueryList() throws Exception { - final String queryString = String.format("start person=(%d,%d) return person.name, person.age", idFor(michael), idFor(testTeam.david)); - final Collection> result = IteratorUtil.asCollection(queryOperations.queryForList(queryString)); - - assertEquals(asList(testTeam.simpleRowFor(michael,"person"),testTeam.simpleRowFor(testTeam.david,"person")),result); - } - - @Test - public void testQueryListOfTypePerson() throws Exception { - final String queryString = String.format("start person=(name_index,name,\"%s\") match (person) <-[:boss]- (boss) return boss", michael.getName()); - final Collection result = IteratorUtil.asCollection(queryOperations.query(queryString, Node.class)); - - assertEquals(asList(nodeFor(testTeam.emil)),result); - - } - - private Node nodeFor(final NodeBacked entity) { - return entity.getPersistentState(); - } - private long idFor(final NodeBacked entity) { - return entity.getNodeId(); - } - - @Test - public void testQueryOtherTeamMembers() throws Exception { - final String queryString = String.format("start person=(%d) match (person)<-[:persons]-(team)-[:persons]->(member) return member", idFor(michael)); - System.out.println("testTeam = " + testTeam.sdg.getPersons()); - final Collection result = IteratorUtil.asCollection(queryOperations.query(queryString, Node.class)); - - assertThat(result, IsCollectionContaining.hasItems(nodeFor(testTeam.david), nodeFor(testTeam.emil))); - - } - - @Test - public void testQueryAllTeamMembersByTeam() throws Exception { - final String queryString = String.format("start team=(Group,name,\"%s\") match (team)-[:persons]->(member) return member", testTeam.sdg.getName()); - final Collection result = IteratorUtil.asCollection(queryOperations.query(queryString, Node.class)); - - assertThat(result, IsCollectionContaining.hasItems(nodeFor(testTeam.david),nodeFor(testTeam.michael))); - } - - @Test - public void testQueryForObjectAsGroup() throws Exception { - final String queryString = String.format("start person=(name_index,name,\"%s\") match (person) <-[:persons]- (team) return team", michael.getName()); - final Node result = queryOperations.queryForObject(queryString, Node.class); - - assertEquals(nodeFor(testTeam.sdg),result); - } - @Test - public void testQueryForObjectAsString() throws Exception { - final String queryString = String.format("start person=(name_index,name,\"%s\") match (person) <-[:persons]- (team) return team.name", michael.getName()); - final String result = queryOperations.queryForObject(queryString, String.class); - - assertEquals(testTeam.sdg.getName(),result); - } - @Test - public void testQueryForObjectAsEnum() throws Exception { - final String queryString = String.format("start person=(name_index,name,\"%s\") return person.personality", michael.getName()); - final Personality result = queryOperations.queryForObject(queryString, Personality.class); - - assertEquals(michael.getPersonality(),result); - } -} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/template/Neo4jTemplateApiTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/template/Neo4jTemplateApiTest.java index 50259e993..619880f88 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/template/Neo4jTemplateApiTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/template/Neo4jTemplateApiTest.java @@ -205,7 +205,7 @@ public class Neo4jTemplateApiTest { @Test public void testCreateNode() throws Exception { - Node node=template.createNode(); + Node node= template.createNode(null); assertNotNull("created node",node); } @@ -273,6 +273,16 @@ public class Neo4jTemplateApiTest { assertSingleResult("node1",template.traverse(referenceNode, description).to(String.class,new PathNodeNameMapper())); } + @Test + public void shouldFindNextNodeViaCypher() throws Exception { + assertSingleResult(node1, template.query("start n=(0) match n-->m return m", null).to(Node.class)); + } + + @Test + public void shouldFindNextNodeViaGremlin() throws Exception { + assertSingleResult(node1, template.execute("g.v(0).out", null).to(Node.class)); + } + @Test public void shouldGetDirectRelationship() throws Exception { assertSingleResult("rel1", template.convert(referenceNode.getRelationships()).to(String.class, new RelationshipNameConverter())); @@ -295,7 +305,7 @@ public class Neo4jTemplateApiTest { @Test public void shouldCreateRelationshipWithProperty() throws Exception { - Relationship relationship = template.createRelationship(referenceNode, node1, HAS,map("name","rel2")); + Relationship relationship = template.createRelationship(referenceNode, node1, HAS,map("name", "rel2")); assertNotNull(relationship); assertEquals(referenceNode, relationship.getStartNode()); assertEquals(node1,relationship.getEndNode()); diff --git a/spring-data-neo4j/src/test/resources/META-INF/graph-named-queries.properties b/spring-data-neo4j/src/test/resources/META-INF/graph-named-queries.properties index d2e4b9180..a92b0a402 100644 --- a/spring-data-neo4j/src/test/resources/META-INF/graph-named-queries.properties +++ b/spring-data-neo4j/src/test/resources/META-INF/graph-named-queries.properties @@ -1 +1 @@ -Person.findTeam=start p=(%d) match (p)<-[:persons]-(group) return group \ No newline at end of file +Person.findTeam=start p=(%person) match (p)<-[:persons]-(group) return group \ No newline at end of file diff --git a/spring-data-neo4j/src/test/resources/org/springframework/data/neo4j/support/Neo4jGraphPersistenceTest-context.xml b/spring-data-neo4j/src/test/resources/org/springframework/data/neo4j/support/Neo4jGraphPersistenceTest-context.xml index 198b29d6f..16d0554e7 100644 --- a/spring-data-neo4j/src/test/resources/org/springframework/data/neo4j/support/Neo4jGraphPersistenceTest-context.xml +++ b/spring-data-neo4j/src/test/resources/org/springframework/data/neo4j/support/Neo4jGraphPersistenceTest-context.xml @@ -150,7 +150,7 @@ - start p=(%d) match (p)<-[:persons]-(group) return group + start p=(%person) match (p)<-[:persons]-(group) return group diff --git a/spring-data-neo4j/template.mf b/spring-data-neo4j/template.mf index 51c18555b..f486700ad 100644 --- a/spring-data-neo4j/template.mf +++ b/spring-data-neo4j/template.mf @@ -27,9 +27,12 @@ Import-Template: javax.validation.*;version="0";resolution:=optional, javax.annotation.*;version="0";resolution:=optional, javax.naming.*;version="0";resolution:=optional, + javax.script.*;version="0";resolution:=optional, javax.persistence.*;version="[1.0.0, 3.0.0)";resolution:=optional, javax.persistence.spi.*;version="[1.0.0, 3.0.0)";resolution:=optional, - javax.transaction.*;version="[1.0.1, 2.0.0)";resolution:=optional + javax.transaction.*;version="[1.0.1, 2.0.0)";resolution:=optional, + com.tinkerpop.blueprints.*;version="[0.8,1.0)";resolution:=optional, + com.tinkerpop.gremlin.*;version="[1.1,2.0)";resolution:=optional Import-Package: net.sf.cglib.proxy;version="[2.2.0,3.0.0)", net.sf.cglib.core;version="[2.2.0,3.0.0)", diff --git a/src/docbkx/index.xml b/src/docbkx/index.xml index 87cb48bbc..ac3ca748e 100644 --- a/src/docbkx/index.xml +++ b/src/docbkx/index.xml @@ -2,6 +2,17 @@ + Good Relationships The Spring Data Graph Guide Book @@ -23,14 +34,6 @@ David Montag - - Mark - Pollack - - - Thomas - Risberg - @@ -40,6 +43,9 @@ further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. + + Copyright 2010-2011 Neo Technology + diff --git a/src/docbkx/introduction/about.xml b/src/docbkx/introduction/about.xml index 916fa350a..b94656469 100644 --- a/src/docbkx/introduction/about.xml +++ b/src/docbkx/introduction/about.xml @@ -3,44 +3,76 @@ "http://www.oasis-open.org/docbook/xml/4.4/docbookx.dtd"> About this guide book - - Welcome to the Spring Data Graph Guide Book. Thank you for taking the time to get an in depth look - into Spring Data Graph. - This project is part of the Spring Data project, - which brings the convenient programming model of the Spring Framework to modern NOSQL databases. - Spring Data Graph, as the name alludes to, aims to provide support for graph databases. It currently - supports Neo4j. - - - It was written by developers for developers. Hopefully we've created a document that is well received - by our peers. - - - If you have any feedback on Spring Data Graph or this book, please provide it via the - SpringSource JIRA, the - SpringSource NOSQL Forum, - github comments or issues, - or the Neo4j mailing list. - - - This book is presented as a duplex book, - a term coined by Martin Fowler. A duplex book consists of at least two parts. The first part is an easily - accessible tutorial that gives the reader an overview of the topics contained in the book. It contains lots - of examples and discussion topics. This part of the book is highly suited for cover-to-cover reading. - - - We chose a tutorial describing the creation of a web application that allows movie enthusiasts to - find their favorite movies, rate them, connect with fellow movie geeks, and enjoy social features such as - recommendations. The application is running on Neo4j using Spring Data Graph and the well-known Spring - Web Stack. - - - The second part of the book is the classic reference documentation, containing detailed information about - the library. It discusses the programming model, the underlying assumptions, and internals, as well as the - APIs for the object-graph mapping. The reference documentation is typically used to look up concrete bits of - information, or to drill down into certain topics. For hackers wanting to really delve into Spring Data - Graph, it can of course also be read cover-to-cover. - +
+ The Spring Data Graph Project + + Welcome to the Spring Data Graph Guide Book. Thank you for taking the time to get an in depth look + into Spring Data Graph. + This project is part of the Spring Data project, + which brings the convenient programming model of the Spring Framework to modern NOSQL databases. + Spring Data Graph, as the name alludes to, aims to provide support for graph databases. It currently + supports Neo4j. + +
+
+ Feedback + + It was written by developers for developers. Hopefully we've created a guide that is well received + by our peers. + + + If you have any feedback on Spring Data Graph or this book, please provide it via the + SpringSource JIRA, the + SpringSource NOSQL Forum, + github comments or issues, + or the Neo4j mailing list. + +
+
Format of the Book + + This book is presented as a duplex book, + a term coined by Martin Fowler. A duplex book consists of at least two parts. The first part is an easily + accessible tutorial that gives the reader an overview of the topics contained in the book. It contains lots + of examples and discussion topics. This part of the book is highly suited for cover-to-cover reading. + + + We chose a tutorial describing the creation of a web application that allows movie enthusiasts to + find their favorite movies, rate them, connect with fellow movie geeks, and enjoy social features such as + recommendations. The application is running on Neo4j using Spring Data Graph and the well-known Spring + Web Stack. + + + The second part of the book is the classic reference documentation, containing detailed information about + the library. It discusses the programming model, the underlying assumptions, and internals, as well as the + APIs for the object-graph mapping. The reference documentation is typically used to look up concrete bits of + information, or to drill down into certain topics. For hackers wanting to really delve into Spring Data + Graph, it can of course also be read cover-to-cover. + +
+
Acknowledgements + + We would like to thank everyone who contributed to this book, especially Mark Pollack and Thomas Risberg, + the leads of the Spring Data Project, who helped a lot during the development of the library as well as sharing + great feedback about the book. Also Oliver Gierke, our local German VMWare/SpringSource engineer, who invested a + lot of time discussing various aspects of the library as well as providing the superb foundations for the Spring + Data Repositories. We tortured Andy Clement, the AspectJ project lead, with many questions and issues around our + advanced AspectJ usage which caused some headaches. He always quickly solved our issues and gave us excellent + answers. + + + We also appreciate very much the foresight of Rod Johnson and Emil Eifrem to initiate the project, and now + also providing great forewords. Their leadership inspired collaboration between the engineering teams at + SpringSource and Neo Technology, a tremendous help during the making of Spring Data Graph. +   + +   + + Last but not least we thank our vibrant community, both in the Spring Forums as well as on the Neo4j + Mailing list and on many other places on the internet for giving us feedback, reporting issues and suggesting + improvements. Without that important feedback we wouldn't be where we are today. +   + +
  Enjoy the book! diff --git a/src/docbkx/reference/neo4j-server.xml b/src/docbkx/reference/neo4j-server.xml index 6c23db072..851b442a8 100644 --- a/src/docbkx/reference/neo4j-server.xml +++ b/src/docbkx/reference/neo4j-server.xml @@ -88,8 +88,8 @@ public void foo( @Context WorldRepository repo ) { Please also keep in mind that performing graph operations via the REST-API is about one order of - magnitude slower than location operations. Try to use the Neo4j-Query-Language or - server-side traversals whenever possible (RestTraversal) for retrieving large sets of data. + magnitude slower than location operations. Try to use the Neo4j Cypher query language, + server-side traversals (RestTraversal) or Gremlin expressions whenever possible for retrieving large sets of data. Future versions of Spring Data Graph will use the more performant batching as well as a binary protocol. @@ -124,5 +124,8 @@ public void foo( @Context WorldRepository repo ) { to the remote instance. (e.g. queryEngineFor(), index() and createTraversalDescription()). Please use those methods when interacting with a remote server for optimal performance. + + + diff --git a/src/docbkx/reference/neo4j.xml b/src/docbkx/reference/neo4j.xml index eec2043c9..bc587b1d4 100644 --- a/src/docbkx/reference/neo4j.xml +++ b/src/docbkx/reference/neo4j.xml @@ -150,6 +150,91 @@ try { for (Node foundNode : nodeIndex.get("property","value")) { // found node } +]]> + + +
+ Querying with Cypher + + With version 1.4.M04 Neo4j introduced a textual query language called + "Cypher" which draws from many + sources. From graph matching like in SPARQL, some keywords and query structure that reminds of SQL and + some iconic representation. A screencast presenting cypher queries on the cineasts.net dataset is available + at video.neo4j.org. Cypher was written + in Scala to leverage the high expressiveness for lazy sequence operations of the language and the great + parser combinator library. + + + Cypher queries always begin with a start set of nodes. Those can be either expressed by their + id's or by a index lookup expression. Those start-nodes are then related to other nodes in the + match clause to other nodes. Start and match clause can introduce new identifiers for nodes and + relationships. In the where clause additional filtering of the result set is applied by evaluating + boolean expressions. The return clause defines which part of the query result will be available. + Aggregation also happens in the return clause by using aggregation functions on some of the values. + Sorting can happen in the order by clause and the skip and limit parts + restrict the result set to a certain window. + + + Cypher can be executed on an embedded graph db using ExecutionEngine and + CypherParser. This is encapsulated in Spring Data Graph with + CypherQueryEngine. The Neo4j-REST-Server comes with a Cypher-Plugin that is accessible remotely and is + available in the Spring Data Graph REST-Binding. + + + Cypher Examples on the Cineasts.net Dataset + (movie) where r.stars > 3 + return movie.title, r.stars, r.comment + +// Mutual Friend recommendations: +start user=(User,login,'micha') match (user)-[:FRIEND]-(friend)-[r,:RATED]->(movie) where r.stars > 3 + return friend.name, movie.title, r.stars, r.comment? + +// Movie suggestions based on a movie: +start movie=(Movie,id,'13') match (movie)<-[:ACTS_IN]-()-[:ACTS_IN]->(suggestion) + return suggestion.title, count(*) order by count(*) desc limit 5 + +// Co-Actors, sorted by count and name of Lucy Liu +start lucy=(1000) match (lucy)-[:ACTS_IN]->(movie)<-[:ACTS_IN]-(co_actor) + return count(*), co_actor.name order by count(*) desc,co_actor.name limit 20 + +// recommendations including counts, grouping and sorting +start user=(User,login,'micha') match (user)-[:FRIEND]-(friend)-[r,:RATED]->(movie) + return movie.title, AVG(r.stars), count(*) order by AVG(r.stars) desc, count(*) desc +]]> + +
+
+ Gremlin a Graph Traversal DSL + + Gremlin is an expressive Groovy DSL developed by Marko Rodriguez + as part of the tinkerpop stack. It builds on top of a pipe implementation + (Blueprints Pipes) that uses connected operations to traverse a graph. Gremlin has a concise syntax but is + turing complete. + + Gremlin can be executed by including the tinkerpop and blueprints dependencies and then requesting a ScriptEngine + of type "gremlin" from the javax.Script* facilities. In Spring Data Graph this is encapsulated in + GremlinQueryEngine. The Neo4j-REST-Server also comes with a Gremlin-Plugin that is accessible remotely and is + available in the Spring Data Graph REST-Binding. + + + Sample Gremlin Queries + 30}.name + +// calculate basic collaborative filtering for vertex 1 +m = [:] +g.v(1).out('likes').in('likes').out('likes').groupCount(m) +m.sort{a,b -> a.value <=> b.value} ]]>
diff --git a/src/docbkx/reference/programming-model/aspectj.xml b/src/docbkx/reference/programming-model/aspectj.xml index 0765890d4..4b18c21ea 100644 --- a/src/docbkx/reference/programming-model/aspectj.xml +++ b/src/docbkx/reference/programming-model/aspectj.xml @@ -48,9 +48,18 @@ The current version that does not show incorrect errors is AspectJ 1.6.12.M1 (included in STS 2.7.0.M2), previous versions are reported to mislead the user. + + + There might be some issues with the eclipse maven plugin not adding AspectJ files correctly to the build path. If you encounter issues, please try the following: + + Try editing the build path to include **/*.aj for the spring-data-neo4j project. + You can do this by selecting "Build Path -> Configure Build Path ..." from the Package Explorer. + Then for the spring-data-neo4j/src/main/java add **/*.aj to the Included path. + + The AspectJ support in IntelliJ IDEA lacks some of the features. JetBrains is working on improving - the situation in their upcoming 10.5 release of their popular IDE. Their latest work is available + the situation in their upcoming 11 release of their popular IDE. Their latest work is available under their early access program (EAP). Building the project with the AspectJ compiler ajc works in IDEA (Options -> Compiler -> Java Compiler should show ajc). Make sure to give the compiler at least 512 MB of RAM. diff --git a/src/docbkx/reference/programming-model/introducedmethods.xml b/src/docbkx/reference/programming-model/introducedmethods.xml index a212334e2..6ac8b04f9 100644 --- a/src/docbkx/reference/programming-model/introducedmethods.xml +++ b/src/docbkx/reference/programming-model/introducedmethods.xml @@ -114,19 +114,19 @@ - Executes the given query, replacing the first %d with the node-id and returning the results converted to the target type. + Executes the given query, replacing %start with the node-id and returning the results converted to the target type. <T> Iterable<T> NodeBacked.findAllByQuery(final String query, final Class<T> targetType) - Executes the given query, replacing the first %d with the node-id and returning the original result, but with nodes and relationships replaced by their appropriate entities. + Executes the given query, replacing %start with the node-id and returning the original result, but with nodes and relationships replaced by their appropriate entities. Iterable<Map<String,Object>> NodeBacked.findAllByQuery(final String query) - Executes the given query, replacing the first %d with the node-id and returns a single result converted to the target type. + Executes the given query, replacing %start with the node-id and returns a single result converted to the target type. <T> T NodeBacked.findByQuery(final String query, final Class<T> targetType) diff --git a/src/docbkx/reference/programming-model/node-entities.xml b/src/docbkx/reference/programming-model/node-entities.xml index e4eb93ace..55acbc6ce 100644 --- a/src/docbkx/reference/programming-model/node-entities.xml +++ b/src/docbkx/reference/programming-model/node-entities.xml @@ -72,28 +72,33 @@ public class Movie {
- @GraphQuery: fields as query result views + @Query: fields as query result views - The @GraphQuery annotation leverages the delegation infrastructure used by the + The @Query annotation leverages the delegation infrastructure used by the Spring Data Graph aspects. It provides dynamic fields which, when accessed, return the values - selected by the provided query language expression. The provided query must contain a placeholder - for the id of the current entity start n=(%d) match n-[:FRIEND]->friend return friend - As graph queries can return variable number of entities the annotation can be put onto fields - with a single value, an Iterable of a type or an Iterable of Map<String,Object>. - The class of the resulting node entities must right now provided with the elementClass attribute. - Additional parameters are added to the query with Java's String.format substitution. + selected by the provided query language expression. The provided query must contain a placeholder named %start + for the id of the current entity. For instance start n=(%start) match n-[:FRIEND]->friend return friend. + Graph queries can return variable number of entities. That's why annotation can be put onto fields + with a single value, an Iterable of a concrete type or an Iterable of Map<String,Object>. + Additional parameters are taken from the params attribute of the @Query annotation. + The tuples form key-value pairs that are provided to the query at execution time. - @GraphQuery from a node entity + @Graph on a node entity field (friend) return friend", - elementClass = Person.class, params = "FRIEND") + @Query(value = "start n=(%start) match (n)-[:%relType]->(friend) return friend", + params = {"relType", "FRIEND"}) private Iterable friends; } ]]> + + + Please note that this annotation can also be used on repository methods. + +
@GraphTraversal: fields as traversal result views diff --git a/src/docbkx/reference/programming-model/repositories.xml b/src/docbkx/reference/programming-model/repositories.xml index d464b1ad9..1038c8abb 100644 --- a/src/docbkx/reference/programming-model/repositories.xml +++ b/src/docbkx/reference/programming-model/repositories.xml @@ -132,13 +132,16 @@
Annotated Queries - Queries for the graph-query language cypher can be supplied with the @GraphQuery annotation. - That means every method annotated with @GraphQuery("start n=(%d) match (n)-->(m) return m") - will use the query string. The String-format parameters are replaced by the actual method parameters in order, - whereby Node and Relationship-Entities are resolved to their respective id's and all other parameters are + Queries for the cypher graph-query language can be supplied with the @Query annotation. + That means every method annotated with @Query("start n=(%node) match (n)-->(m) return m") + will use the query string. The named parameter %node will be replaced by the actual method parameters. + Node and Relationship-Entities are resolved to their respective id's and all other parameters are replaced directly (i.e. Strings, Longs, etc). There is special support for the Sort and Pageable parameters from Spring Data Commons, which are supported to add programmatic paging and sorting (alternatively static paging and sorting can be supplied in the query string itself). + + For using the named parameters you have to either annotate the parameters of the method with the + @Param("node") annotation or enable debug symbols.
@@ -146,8 +149,9 @@ Named Queries Spring Data Graph also supports the notion of named queries which are externalized in property-config-files (META-INF/graph-named-queries.properties). Those files have the format: - Entity.finderName=query (e.g. Person.findBoss=start p=(%d) match (p)<-[:BOSS]-(boss) return boss). - Otherwise named queries support the same parameters as annotated queries. + Entity.finderName=query (e.g. Person.findBoss=start p=(%person) match (p)<-[:BOSS]-(boss) return boss). + Otherwise named queries support the same parameters as annotated queries. For using the named parameters you have to either + annotate the parameters of the method with the @Param("person") annotation or enable debug symbols.
diff --git a/src/docbkx/reference/template.xml b/src/docbkx/reference/template.xml index 18f34fa4e..4179c40bf 100644 --- a/src/docbkx/reference/template.xml +++ b/src/docbkx/reference/template.xml @@ -14,68 +14,89 @@ There are methods (createNode() and createRelationship()) for creating nodes and - relationships that automatically set provided properties and optionally index certain fields. + relationships that automatically set provided properties. Neo4j template - +// Cypher +assert "Mark".equals(neo.query("start p=(%person) match p<-[:WORKS_WITH]-other return other.name", + map("person",thomas)).to(String.class).single()); + +// Gremlin +assert thomas.equals(neo.execute("g.v(person).out('WORKS_WITH')", + map("person",mark)).to(Node.class).single()); + +// Index lookup +assert mark.equals(neo.lookup("devs","name","Mark").single()); + +// Index lookup with Result Converter +assert "Mark".equals(neo.lookup("devs","name","Mark").to(String.class, new ResultConverter { + public String convert(PropertyContainer element, Class type) { + return (String) element.getProperty("name"); + }}));]]>
+
+ QueryResult + + All querying methods of the template return a uniform result type: QueryResult<T> + which is also an Iterable<T>. The query result offers methods of converting each + element to a target type queryResult.to(Type.class) optionally supplying a + ResultConverter<FROM,TO> which takes care of custom conversions. By default most + query methods can already handle conversions from and to: Paths, Nodes, Relationship and GraphEntities + as well as conversions backed by registered ConversionServices. A converted QueryResult<FROM> is an + Iterable<TO>. QueryResults can be limited to a single value using the queryResult.single() + method. It also offers support for a pure callback function using a Handler<T>. + +
Indexing Adding nodes and relationships to an index is done with the index() method. - The query() methods either take a field/value combination to look for exact matches in the - index, or a Lucene query object or string to handle more complex queries. All query() - methods provide Path results to a PathMapper. + The lookup() methods either take a field/value combination to look for exact matches in the + index, or a Lucene query object or string to handle more complex queries. All lookup() + methods return a QueryResult<PropertyContainer> to be used or transformed.
Graph traversal - The traversal methods are at the core of graph operations. As such, they are fully supported in the - Neo4jTemplate. The traverseNext() method traverses to the direct neighbors - of the start node, filtering the relationships according to the parameters. - - + The traversal methods are at the core of graph operations. The traverse() method covers the full traversal operation that takes a TraversalDescription (typically built with the Traversal.description() - DSL) and runs it from the start node. Each path that is returned by the traversal is passed to the - PathMapper to be converted into the desired type. + DSL) and runs it from the given start node. traverse returns a QueryResult<Path> + to be used or transformed.
- Path abstraction and PathMapper + Cypher Queries - For the querying operations Neo4jTemplate unifies the result with the Path abstraction that - comes from Neo4j. Much like a result set, a path contains a chain of nodes() connected by - relationships(), starting at a startNode() and ending at a - endNode(). The lastRelationship() is also available separately. The - Path abstraction also wraps results that contain just nodes or relationships. + The Neo4jTemplate also allows execution of arbitrary Cypher queries. Via the query + methods the statement and parameter-Map are provided. Cypher Queries return tabular results, so the + QueryResult<Map<String,Object>> contains the rows which can be either used as they are + or converted as needed. +
+
+ Gremlin Scripts - Using implementations of PathMapper<T> and PathMapper.WithoutResult - (comparable with RowMapper and RowCallbackHandler), the paths can be converted - to arbitrary Java objects. - - - With EntityPath and EntityMapper there is also support for using - node entities within the Path and PathMapper constructs. + Gremlin Scripts can run with the execute method, which also takes the parameters that will be + available as variables inside the script. The result of the executions is a generic + QueryResult<Object> fit for conversion or usage.
@@ -88,4 +109,11 @@ assert "Mark".equals(neo.query("devs","name","Mark",new NodeNamePathMapper())); or the TransactionTemplate.
+
+ Neo4j REST Server + If the template is configured to use a RestGraphDatabase the expensive operations + like traversals and querying are executed efficiently on the server side by using the REST API to forward + those calls. All the other template methods require single network operations. + +
diff --git a/src/docbkx/resources/images/note.png b/src/docbkx/resources/images/note.png new file mode 100644 index 000000000..ad57f6f72 Binary files /dev/null and b/src/docbkx/resources/images/note.png differ