From e5e97a1ff66487f87f5f28161d5afa5dc0e3d4de Mon Sep 17 00:00:00 2001 From: Michael Hunger Date: Mon, 13 Jun 2011 01:07:27 +0200 Subject: [PATCH] added cypher-queries --- .../data/graph/annotation/GraphQuery.java | 55 +++++++ .../QueryFieldAccessorFactory.java | 100 ++++++++++++ .../neo4j/support/node/Neo4jNodeBacking.aj | 16 ++ .../neo4j/support/query/QueryExecutor.java | 147 ++++++++++++++++++ 4 files changed, 318 insertions(+) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/graph/annotation/GraphQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/fieldaccess/QueryFieldAccessorFactory.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/support/query/QueryExecutor.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/graph/annotation/GraphQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/graph/annotation/GraphQuery.java new file mode 100644 index 000000000..eab560ab5 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/graph/annotation/GraphQuery.java @@ -0,0 +1,55 @@ +/** + * 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.graph.annotation; + +import org.springframework.data.graph.core.FieldTraversalDescriptionBuilder; +import org.springframework.data.graph.core.NodeBacked; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Field that provides access to an iterator which is created by applying the traversal that is built by the supplied + * traversal builder to the current node. The result elements are automatically converted to appropriate element + * entity class instances. + *
+ * @GraphTraversal(traversalBuilder=FriendTraversalBuilder.class, elementClass=Person.class)
+ * Iterable<Person> friends;
+ * 
+ * @author Michael Hunger + * @since 15.09.2010 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface GraphQuery { + /** + * @return Query to be executed %d will be replaced by the node-id of the current entity other placeholders by the given params + */ + String value() default ""; + + /** + * @return target type to convert the single result column (if any) to. + */ + Class elementClass() default Object.class; + + /** + * @return parameters that are replaced in the to the @see query-string + */ + String[] params() default {}; +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/fieldaccess/QueryFieldAccessorFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/fieldaccess/QueryFieldAccessorFactory.java new file mode 100644 index 000000000..4d88d1aff --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/fieldaccess/QueryFieldAccessorFactory.java @@ -0,0 +1,100 @@ +/** + * 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.graph.neo4j.fieldaccess; + +import org.neo4j.graphdb.traversal.TraversalDescription; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.graph.annotation.GraphQuery; +import org.springframework.data.graph.annotation.GraphTraversal; +import org.springframework.data.graph.core.FieldTraversalDescriptionBuilder; +import org.springframework.data.graph.core.NodeBacked; +import org.springframework.data.graph.neo4j.support.node.Neo4jNodeBacking; +import org.springframework.data.graph.neo4j.support.query.QueryExecutor; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + +import static org.springframework.data.graph.neo4j.support.DoReturn.doReturn; + +public class QueryFieldAccessorFactory implements FieldAccessorFactory { + @Override + public boolean accept(final Field f) { + final GraphQuery graphQuery = f.getAnnotation(GraphQuery.class); + return graphQuery != null + && !graphQuery.value().isEmpty(); + } + + + @Override + public FieldAccessor forField(final Field field) { + return new QueryFieldAccessor(field); + } + + /** + * @author Michael Hunger + * @since 12.09.2010 + */ + public static class QueryFieldAccessor implements FieldAccessor { + protected final Field field; + private final String query; + private Class target; + protected String[] params; + private boolean iterableResult; + + public QueryFieldAccessor(final Field field) { + this.field = field; + final GraphQuery graphQuery = field.getAnnotation(GraphQuery.class); + this.target = graphQuery.elementClass(); + this.params = graphQuery.params(); + this.query = graphQuery.value(); + this.iterableResult = Iterable.class.isAssignableFrom(field.getType()); + } + + @Override + public boolean isWriteable(NodeBacked nodeBacked) { + return false; + } + + @Override + public Object setValue(final NodeBacked nodeBacked, final Object newVal) { + throw new InvalidDataAccessApiUsageException("Cannot set readonly query field " + field); + } + + @Override + public Object getValue(final NodeBacked nodeBacked) { + final String queryString = String.format(this.query, createPlaceholderParams(nodeBacked)); + return doReturn(executeQuery(nodeBacked, queryString)); + } + + private Object executeQuery(NodeBacked nodeBacked, String queryString) { + if (iterableResult) { + if (target.equals(Object.class)) return nodeBacked.findAllByQuery(queryString); + nodeBacked.findAllByQuery(queryString, this.target); + } + return nodeBacked.findByQuery(query,this.target); + } + + 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; + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/support/node/Neo4jNodeBacking.aj b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/support/node/Neo4jNodeBacking.aj index 13835707b..8c2f6d507 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/support/node/Neo4jNodeBacking.aj +++ b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/support/node/Neo4jNodeBacking.aj @@ -37,8 +37,10 @@ import javax.persistence.Transient; import javax.persistence.Entity; import org.springframework.data.graph.neo4j.support.path.EntityPathPathIterableWrapper; +import org.springframework.data.graph.neo4j.support.query.QueryExecutor; import java.lang.reflect.Field; +import java.util.Map; import static org.springframework.data.graph.neo4j.support.DoReturn.unwrap; @@ -173,6 +175,20 @@ 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 QueryExecutor executor = new QueryExecutor(Neo4jNodeBacking.aspectOf().graphDatabaseContext); + return executor.query(query); + } + + public T NodeBacked.findByQuery(final String query, final Class targetType) { + final QueryExecutor executor = new QueryExecutor(Neo4jNodeBacking.aspectOf().graphDatabaseContext); + return executor.queryForObject(query, targetType); + } public Iterable> NodeBacked.findAllPathsByTraversal(TraversalDescription traversalDescription) { if (!hasPersistentState()) throw new IllegalStateException("No node attached to " + this); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/support/query/QueryExecutor.java b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/support/query/QueryExecutor.java new file mode 100644 index 000000000..e9270f47c --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/support/query/QueryExecutor.java @@ -0,0 +1,147 @@ +/** + * 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.graph.neo4j.support.query; + +import org.neo4j.cypher.SyntaxError; +import org.neo4j.cypher.commands.Query; +import org.neo4j.cypher.javacompat.CypherParser; +import org.neo4j.cypher.javacompat.ExecutionEngine; +import org.neo4j.cypher.javacompat.ExecutionResult; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; +import org.neo4j.helpers.collection.IterableWrapper; +import org.springframework.core.convert.ConversionService; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.data.graph.core.TypeRepresentationStrategy; +import org.springframework.data.graph.neo4j.support.GraphDatabaseContext; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * @author mh + * @since 10.06.11 + * todo limits + */ +public class QueryExecutor { + private final TypeRepresentationStrategy nodeTypeRepresentationStrategy; + private final ExecutionEngine executionEngine; + private final TypeRepresentationStrategy relationshipTypeRepresentationStrategy; + private final ConversionService conversionService; + + public QueryExecutor(GraphDatabaseContext ctx) { + this.nodeTypeRepresentationStrategy = ctx.getNodeTypeRepresentationStrategy(); + relationshipTypeRepresentationStrategy = ctx.getRelationshipTypeRepresentationStrategy(); + conversionService = ctx.getConversionService(); + this.executionEngine = new ExecutionEngine(ctx.getGraphDatabaseService()); + } + + public Iterable> query(String statement) { + try { + ExecutionResult result = parseAndExecuteQuery(statement); + return convertResult(result); + } catch (Exception e) { + throw new InvalidDataAccessResourceUsageException("Error executing statement " + statement, e); + } + } + + public Iterable query(String statement, Class type) { + try { + ExecutionResult result = parseAndExecuteQuery(statement); + return convertResult(result, type); + } catch (Exception e) { + throw new InvalidDataAccessResourceUsageException("Error executing statement " + statement + " for type " + type, e); + } + } + + public T queryForObject(String statement, Class type) { + try { + ExecutionResult result = parseAndExecuteQuery(statement); + final Iterable convertedResult = convertResult(result, type); + return extractSingleResult(convertedResult); + } catch (Exception e) { + throw new InvalidDataAccessResourceUsageException("Error executing statement " + statement + " for type " + type, e); + } + } + + private T extractSingleResult(Iterable convertedResult) { + final Iterator it = convertedResult.iterator(); + if (!it.hasNext()) throw new InvalidDataAccessResourceUsageException("Expected single result, got none"); + T value = it.hasNext() ? it.next() : null; + if (it.hasNext()) + throw new InvalidDataAccessResourceUsageException("Expected single result, got more than one"); + return value; + } + + private Iterable convertResult(ExecutionResult result, final Class type) { + final List columns = result.columns(); + if (columns.size() != 1) + throw new InvalidDataAccessResourceUsageException("Expected single column of results, got " + columns); + final String column = columns.get(0); + return new IterableWrapper>(result) { + @Override + protected T underlyingObjectToObject(Map row) { + return convertValue(row.get(column), type); + } + }; + } + + private Iterable> convertResult(Iterable> result) { + return new IterableWrapper, Map>(result) { + @Override + protected Map underlyingObjectToObject(Map row) { + for (Map.Entry entry : row.entrySet()) { + Object value = convertValue(entry.getValue()); + if (value != entry.getValue()) { + entry.setValue(value); + } + } + return row; + } + }; + } + + private Object convertValue(Object value) { + if (value instanceof Node) { + return nodeTypeRepresentationStrategy.createEntity((Node) value); + } + if (value instanceof Relationship) { + return relationshipTypeRepresentationStrategy.createEntity((Relationship) value); + } + return value; + } + private T convertValue(Object value,Class type) { + if (value instanceof Node) { + return (T) nodeTypeRepresentationStrategy.createEntity((Node) value,type); + } + if (value instanceof Relationship) { + return (T) relationshipTypeRepresentationStrategy.createEntity((Relationship) value,type); + } + return conversionService.convert(value,type); + } + + private ExecutionResult parseAndExecuteQuery(String statement) { + try { + CypherParser parser = new CypherParser(); + Query query = parser.parse(statement); + return executionEngine.execute(query); + } catch (SyntaxError syntaxError) { + throw new InvalidDataAccessResourceUsageException("Error executing statement " + statement, syntaxError); + } + } +} \ No newline at end of file