added virtual field traversal

This commit is contained in:
Michael Hunger
2010-09-15 08:50:03 +02:00
parent 41660db25e
commit 0712b69a02
7 changed files with 278 additions and 25 deletions

View File

@@ -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);
}

View File

@@ -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<? extends FieldTraversalDescriptionBuilder> traversalBuilder() default FieldTraversalDescriptionBuilder.class;
Class<? extends NodeBacked> elementClass() default NodeBacked.class;
}

View File

@@ -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<ENTITY extends NodeBacked, STATE> implements EntityStateAccessors<ENTITY> {
private final STATE underlyingState;
private final ENTITY entity;
private final Class<? extends ENTITY> type;
private final GraphDatabaseContext graphDatabaseContext;
private final Map<Field,FieldAccessor<ENTITY,?>> fieldAccessors=new HashMap<Field, FieldAccessor<ENTITY,?>>();
private final Map<Field,List<FieldAccessListener<ENTITY,?>>> fieldAccessorListeners=new HashMap<Field, List<FieldAccessListener<ENTITY,?>>>();
private Node node;
private final static Log log= LogFactory.getLog(DefaultEntityStateAccessors.class);
public DefaultEntityStateAccessors(final STATE underlyingState, final ENTITY entity, final Class<? extends ENTITY> 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<? extends ENTITY> 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<ENTITY, ?> 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<ENTITY, ?> accessor = accessorFor(field);
final Object result=accessor!=null ? accessor.setValue(entity, newVal) : newVal;
notifyListeners(field, result);
return result;
}
private FieldAccessor<ENTITY, ?> 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<ENTITY, ?> listener : fieldAccessorListeners.get(field)) {
listener.valueChanged(entity, null, result); // todo oldValue
}
}
}

View File

@@ -32,6 +32,7 @@ public class DelegatingFieldAccessorFactory<T> implements FieldAccessorFactory<T
SingleRelationshipFieldAccessor.factory(),
OneToNRelationshipFieldAccessor.factory(),
ReadOnlyOneToNRelationshipFieldAccessor.factory(),
TraversalFieldAccessor.factory(),
OneToNRelationshipEntityFieldAccessor.factory()
);
final Collection<FieldAccessorListenerFactory<?>> fieldAccessorListenerFactories = Arrays.<FieldAccessorListenerFactory<?>>asList(

View File

@@ -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<NodeBacked, Object> {
protected final Field field;
private final FinderFactory finderFactory;
private final FieldTraversalDescriptionBuilder fieldTraversalDescriptionBuilder;
private Class<? extends NodeBacked> 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<? extends NodeBacked> 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<? extends FieldTraversalDescriptionBuilder> traversalDescriptionClass = graphEntityTraversal.traversalBuilder();
final Constructor<? extends FieldTraversalDescriptionBuilder> constructor = traversalDescriptionClass.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
} catch (Exception e) {
throw new RuntimeException("Error creating TraversalDescription from " + field,e);
}
}
public static FieldAccessorFactory<NodeBacked> factory() {
return new TraversalFieldAccessorFactory();
}
@Configurable
private static class TraversalFieldAccessorFactory implements FieldAccessorFactory<NodeBacked> {
@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<NodeBacked, ?> forField(final Field field) {
return new TraversalFieldAccessor(field, finderFactory);
}
}
}

View File

@@ -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<Person> persons;
@GraphEntityRelationship(type = "persons", elementClass = Person.class)
private Iterable<Person> readOnlyPersons;
@GraphEntityRelationship(type = "persons", direction = Direction.OUTGOING, elementClass = Person.class)
private Collection<Person> persons;
@GraphEntityRelationship(type = "persons", elementClass = Person.class)
private Iterable<Person> readOnlyPersons;
@GraphEntityTraversal(traversalBuilder = PeopleTraversalBuilder.class, elementClass = Person.class)
private Iterable<Person> people;
private String name;
@@ -26,23 +32,36 @@ public class Group {
}
public void setPersons(Collection<Person> 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<Person> getPersons() {
return persons;
}
public Iterable<Person> getReadOnlyPersons() {
return readOnlyPersons;
}
public Collection<Person> getPersons() {
return persons;
}
public void setReadOnlyPersons(Iterable<Person> p) {
readOnlyPersons = p;
}
public Iterable<Person> getReadOnlyPersons() {
return readOnlyPersons;
}
public void setReadOnlyPersons(Iterable<Person> p) {
readOnlyPersons = p;
}
public Iterable<Person> 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());
}
}
}

View File

@@ -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<Person> people = group.getPeople();
final HashSet<Person> found = new HashSet<Person>();
for (Person person : people) {
found.add(person);
}
Assert.assertEquals(Collections.singleton(p),found);
}
@Test
@Transactional
public void testTraverseFromGroupToPeopleWithFinder() {