diff --git a/src/main/java/org/springframework/datastore/graph/api/FieldTraversalDescriptionBuilder.java b/src/main/java/org/springframework/datastore/graph/api/FieldTraversalDescriptionBuilder.java new file mode 100644 index 000000000..ee6f81263 --- /dev/null +++ b/src/main/java/org/springframework/datastore/graph/api/FieldTraversalDescriptionBuilder.java @@ -0,0 +1,13 @@ +package org.springframework.datastore.graph.api; + +import org.neo4j.graphdb.traversal.TraversalDescription; + +import java.lang.reflect.Field; + +/** + * @author Michael Hunger + * @since 15.09.2010 + */ +public interface FieldTraversalDescriptionBuilder { + TraversalDescription build(NodeBacked start, Field field); +} diff --git a/src/main/java/org/springframework/datastore/graph/api/GraphEntityTraversal.java b/src/main/java/org/springframework/datastore/graph/api/GraphEntityTraversal.java new file mode 100644 index 000000000..60f00332a --- /dev/null +++ b/src/main/java/org/springframework/datastore/graph/api/GraphEntityTraversal.java @@ -0,0 +1,17 @@ +package org.springframework.datastore.graph.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Michael Hunger + * @since 15.09.2010 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface GraphEntityTraversal { + Class traversalBuilder() default FieldTraversalDescriptionBuilder.class; + Class elementClass() default NodeBacked.class; +} diff --git a/src/main/java/org/springframework/datastore/graph/neo4j/fieldaccess/DefaultEntityStateAccessors.java b/src/main/java/org/springframework/datastore/graph/neo4j/fieldaccess/DefaultEntityStateAccessors.java new file mode 100644 index 000000000..a95dfd539 --- /dev/null +++ b/src/main/java/org/springframework/datastore/graph/neo4j/fieldaccess/DefaultEntityStateAccessors.java @@ -0,0 +1,105 @@ +package org.springframework.datastore.graph.neo4j.fieldaccess; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.NotInTransactionException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.datastore.graph.api.NodeBacked; +import org.springframework.datastore.graph.neo4j.support.GraphDatabaseContext; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Michael Hunger + * @since 12.09.2010 + */ +public class DefaultEntityStateAccessors implements EntityStateAccessors { + private final STATE underlyingState; + private final ENTITY entity; + private final Class type; + private final GraphDatabaseContext graphDatabaseContext; + private final Map> fieldAccessors=new HashMap>(); + private final Map>> fieldAccessorListeners=new HashMap>>(); + private Node node; + private final static Log log= LogFactory.getLog(DefaultEntityStateAccessors.class); + + + public DefaultEntityStateAccessors(final STATE underlyingState, final ENTITY entity, final Class type, final GraphDatabaseContext graphDatabaseContext) { + this.underlyingState = underlyingState; + this.entity = entity; + this.type = type; + this.graphDatabaseContext = graphDatabaseContext; + createAccessorsAndListeners(type, graphDatabaseContext); + } + + + @Override + public void createAndAssignNode() { + try { + final Node node=graphDatabaseContext.createNode(); + setNode(node); + entity.setUnderlyingNode(node); + log.info("User-defined constructor called on class " + entity.getClass() + "; created Node [" + entity.getUnderlyingNode() +"]; Updating metamodel"); + graphDatabaseContext.postEntityCreation(entity); + } catch(NotInTransactionException e) { + throw new InvalidDataAccessResourceUsageException("Not in a Neo4j transaction.", e); + } + } + + @Override + public ENTITY getEntity() { + return entity; + } + + @Override + public GraphDatabaseContext getGraphDatabaseContext() { + return graphDatabaseContext; + } + + @Override + public void setNode(final Node node) { + this.node = node; + } + + private void createAccessorsAndListeners(final Class type, final GraphDatabaseContext graphDatabaseContext) { + final DelegatingFieldAccessorFactory fieldAccessorFactory = new DelegatingFieldAccessorFactory(graphDatabaseContext); + ReflectionUtils.doWithFields(type, new ReflectionUtils.FieldCallback() { + public void doWith(final Field field) throws IllegalArgumentException, IllegalAccessException { + fieldAccessors.put(field, fieldAccessorFactory.forField(field)); + fieldAccessorListeners.put(field, fieldAccessorFactory.listenersFor(field)); // TODO Bad code + } + }); + } + + @Override + public Object getValue(final Field field) { + final FieldAccessor accessor = accessorFor(field); + if (accessor == null) return null; + else return accessor.getValue(entity); + } + @Override + public Object setValue(final Field field, final Object newVal) { + final FieldAccessor accessor = accessorFor(field); + final Object result=accessor!=null ? accessor.setValue(entity, newVal) : newVal; + notifyListeners(field, result); + return result; + } + + private FieldAccessor accessorFor(final Field field) { + return fieldAccessors.get(field); + } + + private void notifyListeners(final Field field, final Object result) { + if (!fieldAccessorListeners.containsKey(field) || fieldAccessorListeners.get(field) == null) return; + + for (final FieldAccessListener listener : fieldAccessorListeners.get(field)) { + listener.valueChanged(entity, null, result); // todo oldValue + } + } + +} diff --git a/src/main/java/org/springframework/datastore/graph/neo4j/fieldaccess/DelegatingFieldAccessorFactory.java b/src/main/java/org/springframework/datastore/graph/neo4j/fieldaccess/DelegatingFieldAccessorFactory.java index e32dd3453..803201ddd 100644 --- a/src/main/java/org/springframework/datastore/graph/neo4j/fieldaccess/DelegatingFieldAccessorFactory.java +++ b/src/main/java/org/springframework/datastore/graph/neo4j/fieldaccess/DelegatingFieldAccessorFactory.java @@ -32,6 +32,7 @@ public class DelegatingFieldAccessorFactory implements FieldAccessorFactory> fieldAccessorListenerFactories = Arrays.>asList( diff --git a/src/main/java/org/springframework/datastore/graph/neo4j/fieldaccess/TraversalFieldAccessor.java b/src/main/java/org/springframework/datastore/graph/neo4j/fieldaccess/TraversalFieldAccessor.java new file mode 100644 index 000000000..0212e5dd7 --- /dev/null +++ b/src/main/java/org/springframework/datastore/graph/neo4j/fieldaccess/TraversalFieldAccessor.java @@ -0,0 +1,83 @@ +package org.springframework.datastore.graph.neo4j.fieldaccess; + +import org.neo4j.graphdb.traversal.TraversalDescription; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Configurable; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.datastore.graph.api.FieldTraversalDescriptionBuilder; +import org.springframework.datastore.graph.api.GraphEntityTraversal; +import org.springframework.datastore.graph.api.NodeBacked; +import org.springframework.datastore.graph.neo4j.finder.Finder; +import org.springframework.datastore.graph.neo4j.finder.FinderFactory; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + +import static org.springframework.datastore.graph.neo4j.fieldaccess.DoReturn.doReturn; + +/** + * @author Michael Hunger + * @since 12.09.2010 + */ +public class TraversalFieldAccessor implements FieldAccessor { + protected final Field field; + private final FinderFactory finderFactory; + private final FieldTraversalDescriptionBuilder fieldTraversalDescriptionBuilder; + private Class target; + + public TraversalFieldAccessor(final Field field, FinderFactory finderFactory) { + this.field = field; + this.finderFactory = finderFactory; + final GraphEntityTraversal graphEntityTraversal = field.getAnnotation(GraphEntityTraversal.class); + this.target = graphEntityTraversal.elementClass(); + this.fieldTraversalDescriptionBuilder = createTraversalDescription(graphEntityTraversal); + } + + @Override + public Object setValue(final NodeBacked nodeBacked, final Object newVal) { + throw new InvalidDataAccessApiUsageException("Cannot set readonly traversal description field " + field); + } + + @Override + public Object getValue(final NodeBacked nodeBacked) { + final Finder finder = finderFactory.getFinderForClass(target); + final TraversalDescription traversalDescription = fieldTraversalDescriptionBuilder.build(nodeBacked,field); + return doReturn(finder.findAllByTraversal(nodeBacked, traversalDescription)); + } + + + private FieldTraversalDescriptionBuilder createTraversalDescription(final GraphEntityTraversal graphEntityTraversal) { + try { + final Class traversalDescriptionClass = graphEntityTraversal.traversalBuilder(); + final Constructor constructor = traversalDescriptionClass.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } catch (Exception e) { + throw new RuntimeException("Error creating TraversalDescription from " + field,e); + } + } + + public static FieldAccessorFactory factory() { + return new TraversalFieldAccessorFactory(); + } + + @Configurable + private static class TraversalFieldAccessorFactory implements FieldAccessorFactory { + @Autowired + private FinderFactory finderFactory; + + @Override + public boolean accept(final Field f) { + final GraphEntityTraversal graphEntityTraversal = f.getAnnotation(GraphEntityTraversal.class); + return graphEntityTraversal != null + && graphEntityTraversal.traversalBuilder() != TraversalDescription.class + && f.getType().equals(Iterable.class); + } + + + @Override + public FieldAccessor forField(final Field field) { + return new TraversalFieldAccessor(field, finderFactory); + } + } +} diff --git a/src/test/java/org/springframework/datastore/graph/neo4j/Group.java b/src/test/java/org/springframework/datastore/graph/neo4j/Group.java index ea7c44004..b377cd0b0 100644 --- a/src/test/java/org/springframework/datastore/graph/neo4j/Group.java +++ b/src/test/java/org/springframework/datastore/graph/neo4j/Group.java @@ -1,19 +1,25 @@ package org.springframework.datastore.graph.neo4j; -import org.springframework.datastore.graph.api.Direction; -import org.springframework.datastore.graph.api.GraphEntity; -import org.springframework.datastore.graph.api.GraphEntityRelationship; +import org.neo4j.graphdb.DynamicRelationshipType; +import org.neo4j.graphdb.traversal.TraversalDescription; +import org.neo4j.kernel.Traversal; +import org.neo4j.kernel.impl.traversal.TraversalDescriptionImpl; +import org.springframework.datastore.graph.api.*; +import java.lang.reflect.Field; import java.util.Collection; @GraphEntity(fullIndex = true) public class Group { - @GraphEntityRelationship(type = "persons", direction = Direction.OUTGOING, elementClass = Person.class) - private Collection persons; - - @GraphEntityRelationship(type = "persons", elementClass = Person.class) - private Iterable readOnlyPersons; + @GraphEntityRelationship(type = "persons", direction = Direction.OUTGOING, elementClass = Person.class) + private Collection persons; + + @GraphEntityRelationship(type = "persons", elementClass = Person.class) + private Iterable readOnlyPersons; + + @GraphEntityTraversal(traversalBuilder = PeopleTraversalBuilder.class, elementClass = Person.class) + private Iterable people; private String name; @@ -26,23 +32,36 @@ public class Group { } public void setPersons(Collection persons) { - this.persons = persons; - } + this.persons = persons; + } - public void addPerson(Person person) { - persons.add(person); - } + public void addPerson(Person person) { + persons.add(person); + } - public Collection getPersons() { - return persons; - } - - public Iterable getReadOnlyPersons() { - return readOnlyPersons; - } + public Collection getPersons() { + return persons; + } - public void setReadOnlyPersons(Iterable p) { - readOnlyPersons = p; - } - + public Iterable getReadOnlyPersons() { + return readOnlyPersons; + } + + public void setReadOnlyPersons(Iterable p) { + readOnlyPersons = p; + } + + public Iterable getPeople() { + return people; + } + + private static class PeopleTraversalBuilder implements FieldTraversalDescriptionBuilder { + @Override + public TraversalDescription build(NodeBacked start, Field field) { + return new TraversalDescriptionImpl() + .relationships(DynamicRelationshipType.withName("persons")) + .filter(Traversal.returnAllButStartNode()); + + } + } } diff --git a/src/test/java/org/springframework/datastore/graph/neo4j/spi/Neo4jGraphPersistenceTest.java b/src/test/java/org/springframework/datastore/graph/neo4j/spi/Neo4jGraphPersistenceTest.java index b42f8cc9c..0d8f4dd45 100644 --- a/src/test/java/org/springframework/datastore/graph/neo4j/spi/Neo4jGraphPersistenceTest.java +++ b/src/test/java/org/springframework/datastore/graph/neo4j/spi/Neo4jGraphPersistenceTest.java @@ -446,7 +446,22 @@ public class Neo4jGraphPersistenceTest { } Assert.assertEquals(Collections.singleton(p),found); } - + + @Test + @Transactional + public void testTraverseFieldFromGroupToPeople() { + Person p = new Person("Michael", 35); + Group group = new Group(); + group.setName("dev"); + group.addPerson(p); + Iterable people = group.getPeople(); + final HashSet found = new HashSet(); + for (Person person : people) { + found.add(person); + } + Assert.assertEquals(Collections.singleton(p),found); + } + @Test @Transactional public void testTraverseFromGroupToPeopleWithFinder() {