From 5656f08461513a66b1bc78555f034b7c8fb3591f Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Sat, 18 Jun 2011 08:07:02 +0200 Subject: [PATCH] Initial draft of automatic Cypher query creation for repositories. --- .../neo4j/mapping/Neo4JMappingContext.java | 35 +++--- .../neo4j/mapping/Neo4JPersistentEntity.java | 28 +++++ .../mapping/Neo4JPersistentEntityImpl.java | 37 ++++++ .../mapping/Neo4JPersistentProperty.java | 44 ++++++++ .../mapping/Neo4JPersistentPropertyImpl.java | 93 ++++++++++++++++ .../graph/neo4j/mapping/RelationshipInfo.java | 41 +++++++ .../neo4j/repository/query/CypherQuery.java | 66 +++++++++++ .../neo4j/repository/query/MatchClause.java | 105 ++++++++++++++++++ .../neo4j/repository/query/StartClause.java | 21 ++++ .../repository/query/MatchClauseUnitTest.java | 87 +++++++++++++++ 10 files changed, 542 insertions(+), 15 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JPersistentEntity.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JPersistentEntityImpl.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JPersistentProperty.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JPersistentPropertyImpl.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/RelationshipInfo.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/repository/query/CypherQuery.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/repository/query/MatchClause.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/repository/query/StartClause.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/graph/neo4j/repository/query/MatchClauseUnitTest.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JMappingContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JMappingContext.java index 54baa7475..043beabc7 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JMappingContext.java @@ -20,29 +20,34 @@ import java.beans.PropertyDescriptor; import java.lang.reflect.Field; import org.springframework.data.mapping.context.AbstractMappingContext; +import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.TypeInformation; /** + * Neo4J specific {@link MappingContext} implementation. Simply creates {@link Neo4JPersistentEntityImpl} and + * {@link Neo4JPersistentProperty} instances. * * @author Oliver Gierke */ public class Neo4JMappingContext extends AbstractMappingContext, Neo4JPersistentProperty> { - /* (non-Javadoc) - * @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation) - */ - @Override - protected Neo4JPersistentEntityImpl createPersistentEntity(TypeInformation typeInformation) { - return new Neo4JPersistentEntityImpl(typeInformation); - } + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation) + */ + @Override + protected Neo4JPersistentEntityImpl createPersistentEntity(TypeInformation typeInformation) { + return new Neo4JPersistentEntityImpl(typeInformation); + } - /* (non-Javadoc) - * @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentProperty(java.lang.reflect.Field, java.beans.PropertyDescriptor, org.springframework.data.mapping.model.MutablePersistentEntity, org.springframework.data.mapping.model.SimpleTypeHolder) - */ - @Override - protected Neo4JPersistentProperty createPersistentProperty(Field field, PropertyDescriptor descriptor, - Neo4JPersistentEntityImpl owner, SimpleTypeHolder simpleTypeHolder) { - return new Neo4JPersistentPropertyImpl(field, descriptor, owner, simpleTypeHolder); - } + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentProperty(java.lang.reflect.Field, java.beans.PropertyDescriptor, org.springframework.data.mapping.model.MutablePersistentEntity, org.springframework.data.mapping.model.SimpleTypeHolder) + */ + @Override + protected Neo4JPersistentProperty createPersistentProperty(Field field, PropertyDescriptor descriptor, + Neo4JPersistentEntityImpl owner, SimpleTypeHolder simpleTypeHolder) { + return new Neo4JPersistentPropertyImpl(field, descriptor, owner, simpleTypeHolder); + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JPersistentEntity.java b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JPersistentEntity.java new file mode 100644 index 000000000..10517cfdf --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JPersistentEntity.java @@ -0,0 +1,28 @@ +/** + * 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.mapping; + +import org.springframework.data.mapping.PersistentEntity; + +/** + * Interface for Neo4J specific {@link PersistentEntity}. + * + * @author Oliver Gierke + */ +public interface Neo4JPersistentEntity extends PersistentEntity { + +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JPersistentEntityImpl.java b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JPersistentEntityImpl.java new file mode 100644 index 000000000..67211ad48 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JPersistentEntityImpl.java @@ -0,0 +1,37 @@ +/** + * 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.mapping; + +import org.springframework.data.mapping.model.BasicPersistentEntity; +import org.springframework.data.util.TypeInformation; + +/** + * Implementation of {@link Neo4JPersistentEntity}. + * + * @author Oliver Gierke + */ +class Neo4JPersistentEntityImpl extends BasicPersistentEntity implements Neo4JPersistentEntity { + + /** + * Creates a new {@link Neo4JPersistentEntityImpl} instance. + * + * @param information must not be {@literal null}. + */ + public Neo4JPersistentEntityImpl(TypeInformation information) { + super(information); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JPersistentProperty.java b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JPersistentProperty.java new file mode 100644 index 000000000..37a7aa5a4 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JPersistentProperty.java @@ -0,0 +1,44 @@ +/** + * 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.mapping; + +import org.springframework.data.mapping.PersistentProperty; + +/** + * Interface for Neo4J specific {@link PersistentProperty}s. Declares additional metadata to lookup relationship + * information. + * + * @author Oliver Gierke + */ +public interface Neo4JPersistentProperty extends PersistentProperty { + + /** + * Returns whether the property represents a relationship. If this returns {@literal true}, clients can expect + * {@link #getRelationshipInfo()} to return a non-{@literal null} value. + * + * @return + */ + boolean isRelationship(); + + /** + * Returns the {@link RelationshipInfo} for the given property if it is a relationship or {@literal null} otherwise. + * + * @see #isRelationship() + * @return + */ + RelationshipInfo getRelationshipInfo(); +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JPersistentPropertyImpl.java b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JPersistentPropertyImpl.java new file mode 100644 index 000000000..b7d3c6725 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/Neo4JPersistentPropertyImpl.java @@ -0,0 +1,93 @@ +/** + * 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.mapping; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; + +import org.springframework.data.graph.annotation.GraphId; +import org.springframework.data.graph.annotation.RelatedTo; +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.model.AbstractPersistentProperty; +import org.springframework.data.mapping.model.SimpleTypeHolder; + +/** + * Implementation of {@link Neo4JPersistentProperty}. + * + * @author Oliver Gierke + */ +class Neo4JPersistentPropertyImpl extends AbstractPersistentProperty implements +Neo4JPersistentProperty { + + private final RelationshipInfo relationshipInfo; + private final boolean isIdProperty; + + /** + * Creates a new {@link Neo4JPersistentPropertyImpl} from the given {@link Field}, {@link PropertyDescriptor} owner + * {@link PersistentEntity} and {@link SimpleTypeHolder}. + * + * @param field + * @param propertyDescriptor + * @param owner + * @param simpleTypeHolder + */ + public Neo4JPersistentPropertyImpl(Field field, PropertyDescriptor propertyDescriptor, + PersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { + super(field, propertyDescriptor, owner, simpleTypeHolder); + + this.relationshipInfo = field.isAnnotationPresent(RelatedTo.class) ? new AnnotationBasedRelationshipInfo( + field.getAnnotation(RelatedTo.class)) : null; + this.isIdProperty = field.isAnnotationPresent(GraphId.class); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.PersistentProperty#isIdProperty() + */ + @Override + public boolean isIdProperty() { + return this.isIdProperty; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.AbstractPersistentProperty#createAssociation() + */ + @Override + protected Association createAssociation() { + return new Association(this, null); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.graph.neo4j.mapping.Neo4JPersistentProperty#isRelationship() + */ + @Override + public boolean isRelationship() { + return this.relationshipInfo != null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.graph.neo4j.mapping.Neo4JPersistentProperty#getRelationShipInfo() + */ + @Override + public RelationshipInfo getRelationshipInfo() { + return relationshipInfo; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/RelationshipInfo.java b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/RelationshipInfo.java new file mode 100644 index 000000000..0e4bdfac4 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/mapping/RelationshipInfo.java @@ -0,0 +1,41 @@ +/** + * 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.mapping; + +import org.springframework.data.graph.core.Direction; + +/** + * Captures information about a relationship. + * + * @author Oliver Gierke + */ +public interface RelationshipInfo { + + /** + * Returns the direction of the relationship. + * + * @return + */ + Direction getDirection(); + + /** + * Returns the type of the relationship. + * + * @return + */ + String getType(); +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/repository/query/CypherQuery.java b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/repository/query/CypherQuery.java new file mode 100644 index 000000000..0ada9f34f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/repository/query/CypherQuery.java @@ -0,0 +1,66 @@ +/** + * 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.repository.query; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.data.graph.neo4j.mapping.Neo4JMappingContext; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Value object to create {@link CypherQuery} instances. + * + * @author Oliver Gierke + */ +public class CypherQuery { + + private final String TEMPLATE = "start %s match % where %"; + + private final Neo4JMappingContext context; + private final TypeInformation rootType; + + private final Set matchClauses; + + public CypherQuery(TypeInformation rootType, Neo4JMappingContext context) { + + Assert.notNull(rootType); + Assert.notNull(context); + + this.rootType = rootType; + this.context = context; + this.matchClauses = new HashSet(); + } + + public CypherQuery addRestriction(Part part) { + + matchClauses.add(new MatchClause(context, part.getProperty())); + return this; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format(TEMPLATE, "TODO", StringUtils.collectionToCommaDelimitedString(matchClauses), "TODO"); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/repository/query/MatchClause.java b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/repository/query/MatchClause.java new file mode 100644 index 000000000..68e77f3d4 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/repository/query/MatchClause.java @@ -0,0 +1,105 @@ +/** + * 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.repository.query; + +import org.springframework.data.graph.neo4j.mapping.Neo4JMappingContext; +import org.springframework.data.graph.neo4j.mapping.Neo4JPersistentProperty; +import org.springframework.data.graph.neo4j.mapping.RelationshipInfo; +import org.springframework.data.repository.query.parser.Property; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Value object to build the {@code match} clause of a Cypher query. + * + * @author Oliver Gierke + */ +class MatchClause { + + private final Iterable properties; + + /** + * Creates a new {@link MatchClause} using the given {@link Neo4JMappingContext} and {@link Property}. + * + * @param context must not be {@literal null}. + * @param property must not be {@literal null}. + */ + public MatchClause(Neo4JMappingContext context, Property property) { + + Assert.notNull(context); + Assert.notNull(property); + + Class rootType = property.getOwningType().getType(); + this.properties = context.getPersistentPropertyPath(rootType, property.toDotPath()); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + String intermediate = null; + + for (Neo4JPersistentProperty property : properties) { + + if (!property.isRelationship()) { + return intermediate; + } + + RelationshipInfo info = property.getRelationshipInfo(); + Class ownerType = property.getOwner().getType(); + + intermediate = intermediate == null ? asVariableReference(StringUtils.uncapitalize(ownerType.getSimpleName())) + : intermediate; + intermediate = String.format(getPattern(info), intermediate, info.getType(), asVariableReference(property.getName())); + } + + return intermediate.toString(); + } + + /** + * Returns the given value as variable reference. + * + * @param value + * @return + */ + private static String asVariableReference(String value) { + return String.format("(%s)", value); + } + + /** + * Returns the clause pattern for the given {@link RelationshipInfo}. + * + * @param info must not be {@literal null}. + * @return + */ + private static String getPattern(RelationshipInfo info) { + + switch (info.getDirection()) { + case OUTGOING: + return "%s-[:%s]->%s"; + case INCOMING: + return "%s<-[:%s]-%s"; + case BOTH: + return "%s-[:%s]-%s"; + default: + throw new IllegalArgumentException("Unsupported direction!"); + } + } +} \ No newline at end of file diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/repository/query/StartClause.java b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/repository/query/StartClause.java new file mode 100644 index 000000000..06098d063 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/graph/neo4j/repository/query/StartClause.java @@ -0,0 +1,21 @@ +/** + * 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.repository.query; + +class StartClause { + +} \ No newline at end of file diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/graph/neo4j/repository/query/MatchClauseUnitTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/graph/neo4j/repository/query/MatchClauseUnitTest.java new file mode 100644 index 000000000..b3230ade0 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/graph/neo4j/repository/query/MatchClauseUnitTest.java @@ -0,0 +1,87 @@ +/** + * 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.repository.query; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.util.Collections; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.graph.annotation.NodeEntity; +import org.springframework.data.graph.annotation.RelatedTo; +import org.springframework.data.graph.core.Direction; +import org.springframework.data.graph.neo4j.annotation.Indexed; +import org.springframework.data.graph.neo4j.mapping.Neo4JMappingContext; +import org.springframework.data.repository.query.parser.Property; + +/** + * + * @author Oliver Gierke + */ +public class MatchClauseUnitTest { + + Neo4JMappingContext context; + + @Before + public void setUp() { + context = new Neo4JMappingContext(); + context.setInitialEntitySet(Collections.singleton(Person.class)); + context.afterPropertiesSet(); + } + + @Test + public void buildsMatchExpressionForSimpleTraversalCorrectly() { + + MatchClause clause = new MatchClause(context, Property.from("group", Person.class)); + assertThat(clause.toString(), is("(person)<-[:members]-(group)")); + } + + @Test + public void createsMatchClassForDeepTraversal() { + + MatchClause clause = new MatchClause(context, Property.from("group.members.age", Person.class)); + assertThat(clause.toString(), is("(person)<-[:members]-(group)-[:members]->(members)")); + } + + @Test + public void stopsAtNonRelationShipProperty() { + + MatchClause clause = new MatchClause(context, Property.from("group.name", Person.class)); + assertThat(clause.toString(), is("(person)<-[:members]-(group)")); + } + + @NodeEntity + class Person { + + private int age; + + @RelatedTo(type = "members", direction = Direction.INCOMING) + private Group group; + } + + class Group { + + @Indexed + private String name; + + @RelatedTo(type = "members", direction = Direction.OUTGOING) + private Set members; + } +}