From 9e87a6631067e285f9ecfee099136e808f8adcae Mon Sep 17 00:00:00 2001 From: Michael Hunger Date: Mon, 30 Aug 2010 05:35:25 +0200 Subject: [PATCH] very large changeset repackaged datastore-graph added support for keeping state outside of the transaction + dirty handling to the Neo4jNodeBacked Aspect and flushing it on reconnect added transaction handling for the EntityManager had to add handling for mapped classes -> add them to persistence.xml and use as lookup list for query target add Query support, including first and maxresults issues with EM creation + JPADialect --- .../fieldaccess/FieldAccessorFactory.java | 2 + .../datastore/graph/neo4j/finder/Finder.java | 2 + .../graph/neo4j/finder/FinderFactory.java | 29 +- .../datastore/graph/neo4j/jpa/Neo4JQuery.java | 155 +++++++++++ .../graph/neo4j/jpa/Neo4jEntityManager.java | 39 ++- .../neo4j/jpa/Neo4jEntityManagerFactory.java | 16 +- .../neo4j/jpa/Neo4jPersistenceProvider.java | 20 +- .../graph/neo4j/spi/node/Neo4jHelper.java | 46 ++- .../graph/neo4j/spi/node/Neo4jNodeBacking.aj | 262 ++++++++++++++---- .../neo4j/spi/node/ShouldProceedOrReturn.java | 25 ++ .../relationship/Neo4jRelationshipBacking.aj | 10 +- .../graph/neo4j/Person_Graph_Entity.aj | 3 +- .../neo4j/jpa/Neo4jEntityManagerTest.java | 56 +++- .../neo4j/spi/Neo4jGraphPersistenceTest.java | 37 ++- src/test/resources/META-INF/persistence.xml | 3 + .../spi/Neo4jGraphPersistenceTest-context.xml | 4 +- 16 files changed, 596 insertions(+), 113 deletions(-) create mode 100644 src/main/java/org/springframework/datastore/graph/neo4j/jpa/Neo4JQuery.java create mode 100644 src/main/java/org/springframework/datastore/graph/neo4j/spi/node/ShouldProceedOrReturn.java diff --git a/src/main/java/org/springframework/datastore/graph/neo4j/fieldaccess/FieldAccessorFactory.java b/src/main/java/org/springframework/datastore/graph/neo4j/fieldaccess/FieldAccessorFactory.java index a8e013e99..a15890d92 100644 --- a/src/main/java/org/springframework/datastore/graph/neo4j/fieldaccess/FieldAccessorFactory.java +++ b/src/main/java/org/springframework/datastore/graph/neo4j/fieldaccess/FieldAccessorFactory.java @@ -1,6 +1,7 @@ package org.springframework.datastore.graph.neo4j.fieldaccess; import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.util.Collection; import org.neo4j.graphdb.Direction; @@ -23,6 +24,7 @@ public class FieldAccessorFactory { } public FieldAccessor forField(Field field) { + if (Modifier.isTransient(field.getModifiers())) return null; GraphEntityRelationship relAnnotation = field.getAnnotation(GraphEntityRelationship.class); if (isSingleRelationshipField(field)) { Class relatedType = (Class) field.getType(); diff --git a/src/main/java/org/springframework/datastore/graph/neo4j/finder/Finder.java b/src/main/java/org/springframework/datastore/graph/neo4j/finder/Finder.java index 93d3664ae..b94344b84 100644 --- a/src/main/java/org/springframework/datastore/graph/neo4j/finder/Finder.java +++ b/src/main/java/org/springframework/datastore/graph/neo4j/finder/Finder.java @@ -1,6 +1,7 @@ package org.springframework.datastore.graph.neo4j.finder; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.neo4j.graphdb.Direction; @@ -30,6 +31,7 @@ public class Finder { public Iterable findAll() { Node subrefNode = Neo4jHelper.findSubreferenceNode(clazz, graphDatabaseService); + if (subrefNode==null) return Collections.emptyList(); // TODO add lazy list on top of graph List result = new ArrayList((int) count()); for (Relationship rel : subrefNode.getRelationships(Neo4jHelper.INSTANCE_OF_RELATIONSHIP_TYPE, Direction.INCOMING)) { diff --git a/src/main/java/org/springframework/datastore/graph/neo4j/finder/FinderFactory.java b/src/main/java/org/springframework/datastore/graph/neo4j/finder/FinderFactory.java index 2633ab3ec..2d1fa1888 100644 --- a/src/main/java/org/springframework/datastore/graph/neo4j/finder/FinderFactory.java +++ b/src/main/java/org/springframework/datastore/graph/neo4j/finder/FinderFactory.java @@ -3,20 +3,29 @@ package org.springframework.datastore.graph.neo4j.finder; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Node; import org.springframework.datastore.graph.api.NodeBacked; +import org.springframework.datastore.graph.neo4j.spi.node.Neo4jHelper; import org.springframework.persistence.support.EntityInstantiator; public class FinderFactory { - - private final GraphDatabaseService graphDatabaseService; - private final EntityInstantiator graphEntityInstantiator; - public FinderFactory(GraphDatabaseService graphDatabaseService, EntityInstantiator graphEntityInstantiator) { - this.graphDatabaseService = graphDatabaseService; - this.graphEntityInstantiator = graphEntityInstantiator; - } + private final GraphDatabaseService graphDatabaseService; + private final EntityInstantiator graphEntityInstantiator; - public Finder getFinderForClass(Class clazz) { - return new Finder(clazz, graphDatabaseService, graphEntityInstantiator); - } + public FinderFactory(GraphDatabaseService graphDatabaseService, EntityInstantiator graphEntityInstantiator) { + this.graphDatabaseService = graphDatabaseService; + this.graphEntityInstantiator = graphEntityInstantiator; + } + public Finder getFinderForClass(Class clazz) { + return new Finder(clazz, graphDatabaseService, graphEntityInstantiator); + } + + public Class getEntityClass(String shortName) { + final String className = Neo4jHelper.getClassNameForShortName(graphDatabaseService, shortName); + try { + return (Class) Class.forName(className); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Unable to find class for " + shortName); + } + } } diff --git a/src/main/java/org/springframework/datastore/graph/neo4j/jpa/Neo4JQuery.java b/src/main/java/org/springframework/datastore/graph/neo4j/jpa/Neo4JQuery.java new file mode 100644 index 000000000..01a7d25f5 --- /dev/null +++ b/src/main/java/org/springframework/datastore/graph/neo4j/jpa/Neo4JQuery.java @@ -0,0 +1,155 @@ +package org.springframework.datastore.graph.neo4j.jpa; + +import org.springframework.datastore.graph.api.NodeBacked; +import org.springframework.datastore.graph.neo4j.finder.Finder; +import org.springframework.datastore.graph.neo4j.finder.FinderFactory; + +import javax.persistence.FlushModeType; +import javax.persistence.Query; +import javax.persistence.TemporalType; +import javax.persistence.spi.PersistenceUnitInfo; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** +* @author Michael Hunger +* @since 29.08.2010 +*/ +public class Neo4JQuery implements Query { + protected final Finder finder; + protected final Class entityClass; + protected final String qlString; + private final PersistenceUnitInfo info; + private final Pattern fromPattern = Pattern.compile("^.*\\bfrom\\s+([A-Z][A-Za-z0-9]+)\\b.*"); + private int startPosition=0; + private int maxResult=-1; + private QueryExectuor queryExectuor; + + public Neo4JQuery(final String qlString, final FinderFactory finderFactory, final PersistenceUnitInfo info) { + this.qlString = qlString; + this.info = info; + final Matcher matcher = fromPattern.matcher(qlString); + if (matcher.matches()) { + final String shortName = matcher.group(1); + entityClass=getEntityClass(shortName); + finder = finderFactory.getFinderForClass(entityClass); + queryExectuor = createExecutor(qlString); + } else { + throw new IllegalAccessError("Unable to parse query "+qlString); + } + } + + private QueryExectuor createExecutor(String qlString) { + if (qlString.contains(" count(")) return new QueryExectuor() { + @Override + protected Iterable findList() { + return Collections.singleton(finder.count()); + } + }; + return new QueryExectuor() { + @Override + protected Iterable findList() { + return finder.findAll(); + } + }; + } + + abstract static class QueryExectuor { + protected abstract Iterable findList(); + + } + + private Class getEntityClass(final String shortName) { + try { + final String className = getFQN(shortName); + return (Class) Class.forName(className); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Error resolving class "+shortName,e); + } + } + + private String getFQN(final String shortName) throws ClassNotFoundException { + for (final String className : info.getManagedClassNames()) { + if (className.endsWith(shortName)) return className; + } + throw new ClassNotFoundException("No mapped class found for "+shortName); + } + + @Override + public List getResultList() { + final List result = new ArrayList(); + int count=0; + for (final Object nodeBacked : queryExectuor.findList()) { + if (maxResult>=0 && count==startPosition+maxResult) break; + if (count>=startPosition) { + result.add(nodeBacked); + } + count++; + } + return result; + } + + @Override + public Object getSingleResult() { + final Iterator found = queryExectuor.findList().iterator(); + return found.hasNext() ? found.next() : null; // todo errors when none or too many ? + } + + @Override + public int executeUpdate() { + return 0; + } + + @Override + public Query setMaxResults(final int maxResult) { + this.maxResult = maxResult; + return this; + } + + @Override + public Query setFirstResult(final int startPosition) { + this.startPosition = startPosition; + return this; + } + + @Override + public Query setHint(final String hintName, final Object value) { + return this; + } + + @Override + public Query setParameter(final String name, final Object value) { + return this; + } + + @Override + public Query setParameter(final String name, final Date value, final TemporalType temporalType) { + return this; + } + + @Override + public Query setParameter(final String name, final Calendar value, final TemporalType temporalType) { + return this; + } + + @Override + public Query setParameter(final int position, final Object value) { + return this; + } + + @Override + public Query setParameter(final int position, final Date value, final TemporalType temporalType) { + return this; + } + + @Override + public Query setParameter(final int position, final Calendar value, final TemporalType temporalType) { + return this; + } + + @Override + public Query setFlushMode(final FlushModeType flushMode) { + return this; + } +} diff --git a/src/main/java/org/springframework/datastore/graph/neo4j/jpa/Neo4jEntityManager.java b/src/main/java/org/springframework/datastore/graph/neo4j/jpa/Neo4jEntityManager.java index bde4c13b8..556dd3ac0 100644 --- a/src/main/java/org/springframework/datastore/graph/neo4j/jpa/Neo4jEntityManager.java +++ b/src/main/java/org/springframework/datastore/graph/neo4j/jpa/Neo4jEntityManager.java @@ -1,15 +1,20 @@ package org.springframework.datastore.graph.neo4j.jpa; -import org.neo4j.graphdb.GraphDatabaseService; -import org.neo4j.graphdb.Node; -import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.*; +import org.neo4j.graphdb.Transaction; import org.neo4j.kernel.EmbeddedGraphDatabase; +import org.springframework.beans.factory.annotation.Configurable; import org.springframework.datastore.graph.api.NodeBacked; +import org.springframework.datastore.graph.neo4j.finder.FinderFactory; import org.springframework.persistence.support.EntityInstantiator; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import javax.persistence.*; +import javax.persistence.spi.PersistenceUnitInfo; import javax.transaction.*; +import java.util.Map; /** * @author Michael Hunger @@ -17,20 +22,27 @@ import javax.transaction.*; * TODO Relationships */ //@Service +@Transactional +@Configurable public class Neo4jEntityManager implements EntityManager { - @Resource GraphDatabaseService graphDatabaseService; - - @Resource EntityInstantiator nodeInstantiator; - private volatile boolean closed; + private PersistenceUnitInfo info; - public Neo4jEntityManager(final GraphDatabaseService graphDatabaseService, final EntityInstantiator nodeInstantiator) { + private Map params; + private volatile boolean closed; + private final FinderFactory finderFactory; + + public Neo4jEntityManager(final GraphDatabaseService graphDatabaseService, final EntityInstantiator nodeInstantiator, PersistenceUnitInfo info, Map params) { this.graphDatabaseService = graphDatabaseService; this.nodeInstantiator = nodeInstantiator; + this.info = info; + this.params = params; + finderFactory = new FinderFactory(graphDatabaseService, nodeInstantiator); } public Neo4jEntityManager() { + finderFactory = new FinderFactory(graphDatabaseService, nodeInstantiator); } private Node nodeFor(final Object entity) { @@ -44,6 +56,13 @@ public class Neo4jEntityManager implements EntityManager { @Override public void persist(final Object entity) { checkClosed(); + final Transaction tx = graphDatabaseService.beginTx(); + try { + + tx.success(); + } finally { + tx.finish(); + } } @Override @@ -102,6 +121,7 @@ public class Neo4jEntityManager implements EntityManager { @Override public void refresh(final Object entity) { nodeFor(entity); + // todo NodeBacked.refresh -> discard dirty } /** @@ -129,7 +149,7 @@ public class Neo4jEntityManager implements EntityManager { @Override public Query createQuery(final String qlString) { checkClosed(); - throw new UnsupportedOperationException(); + return new Neo4JQuery(qlString, finderFactory,info); } /* @@ -209,4 +229,5 @@ public class Neo4jEntityManager implements EntityManager { return new Neo4jEntityTransaction(transactionManager); } + } diff --git a/src/main/java/org/springframework/datastore/graph/neo4j/jpa/Neo4jEntityManagerFactory.java b/src/main/java/org/springframework/datastore/graph/neo4j/jpa/Neo4jEntityManagerFactory.java index 76b0694dd..1fe47147f 100644 --- a/src/main/java/org/springframework/datastore/graph/neo4j/jpa/Neo4jEntityManagerFactory.java +++ b/src/main/java/org/springframework/datastore/graph/neo4j/jpa/Neo4jEntityManagerFactory.java @@ -8,6 +8,7 @@ import org.springframework.persistence.support.EntityInstantiator; import javax.annotation.Resource; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; +import javax.persistence.spi.PersistenceUnitInfo; import java.util.Map; /** @@ -15,28 +16,27 @@ import java.util.Map; * @since 23.08.2010 */ public class Neo4jEntityManagerFactory implements EntityManagerFactory { - @Resource GraphDatabaseService graphDatabaseService; - @Resource EntityInstantiator nodeInstantiator; + private PersistenceUnitInfo info; + private Map params; - public Neo4jEntityManagerFactory() { - } - - public Neo4jEntityManagerFactory(GraphDatabaseService graphDatabaseService, EntityInstantiator nodeInstantiator) { + public Neo4jEntityManagerFactory(GraphDatabaseService graphDatabaseService, EntityInstantiator nodeInstantiator, PersistenceUnitInfo info, Map params) { this.graphDatabaseService = graphDatabaseService; this.nodeInstantiator = nodeInstantiator; + this.info = info; + this.params = params; } @Override public EntityManager createEntityManager() { - return new Neo4jEntityManager(graphDatabaseService,nodeInstantiator); + return new Neo4jEntityManager(graphDatabaseService,nodeInstantiator,info,params); } /* TODO handle different directories for target datastore */ @Override public EntityManager createEntityManager(Map map) { - return new Neo4jEntityManager(graphDatabaseService,nodeInstantiator); + return new Neo4jEntityManager(graphDatabaseService,nodeInstantiator,info,params); } @Override diff --git a/src/main/java/org/springframework/datastore/graph/neo4j/jpa/Neo4jPersistenceProvider.java b/src/main/java/org/springframework/datastore/graph/neo4j/jpa/Neo4jPersistenceProvider.java index 752ce62b2..c916ab872 100644 --- a/src/main/java/org/springframework/datastore/graph/neo4j/jpa/Neo4jPersistenceProvider.java +++ b/src/main/java/org/springframework/datastore/graph/neo4j/jpa/Neo4jPersistenceProvider.java @@ -2,7 +2,9 @@ package org.springframework.datastore.graph.neo4j.jpa; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Node; +import org.springframework.beans.factory.annotation.Configurable; import org.springframework.datastore.graph.api.NodeBacked; +import org.springframework.datastore.graph.neo4j.spi.node.Neo4jHelper; import org.springframework.persistence.support.EntityInstantiator; import javax.annotation.Resource; @@ -16,6 +18,7 @@ import java.util.Map; * @since 23.08.2010 */ // todo handle additinal info + database path +@Configurable public class Neo4jPersistenceProvider implements PersistenceProvider { @Resource GraphDatabaseService graphDatabaseService; @@ -24,12 +27,21 @@ public class Neo4jPersistenceProvider implements PersistenceProvider { EntityInstantiator graphEntityInstantiator; @Override - public EntityManagerFactory createEntityManagerFactory(String emName, Map map) { - return new Neo4jEntityManagerFactory(graphDatabaseService,graphEntityInstantiator); + public EntityManagerFactory createEntityManagerFactory(String emName, Map params) { + System.out.println("emName = " + emName); + System.out.println("params = " + params); + return new Neo4jEntityManagerFactory(graphDatabaseService,graphEntityInstantiator,null,params); } @Override - public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map map) { - return new Neo4jEntityManagerFactory(graphDatabaseService,graphEntityInstantiator); + public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map params) { + System.out.println("info.getPersistenceProviderClassName() = " + info.getPersistenceProviderClassName()); + System.out.println("info.getManagedClassNames() = " + info.getManagedClassNames()); + System.out.println("info.getMappingFileNames() = " + info.getMappingFileNames()); + System.out.println("info.getTransactionType() = " + info.getTransactionType()); + System.out.println("info.getProperties() = " + info.getProperties()); + System.out.println("info.getPersistenceUnitName() = " + info.getPersistenceUnitName()); + System.out.println("params = " + params); + return new Neo4jEntityManagerFactory(graphDatabaseService,graphEntityInstantiator,info,params); } } diff --git a/src/main/java/org/springframework/datastore/graph/neo4j/spi/node/Neo4jHelper.java b/src/main/java/org/springframework/datastore/graph/neo4j/spi/node/Neo4jHelper.java index 1ad914960..f79801ae9 100644 --- a/src/main/java/org/springframework/datastore/graph/neo4j/spi/node/Neo4jHelper.java +++ b/src/main/java/org/springframework/datastore/graph/neo4j/spi/node/Neo4jHelper.java @@ -1,12 +1,12 @@ package org.springframework.datastore.graph.neo4j.spi.node; -import org.neo4j.graphdb.DynamicRelationshipType; -import org.neo4j.graphdb.GraphDatabaseService; -import org.neo4j.graphdb.Node; -import org.neo4j.graphdb.Relationship; -import org.neo4j.graphdb.RelationshipType; +import org.neo4j.graphdb.*; +import org.neo4j.graphdb.Direction; import org.neo4j.util.GraphDatabaseUtil; -import org.springframework.datastore.graph.api.NodeBacked; +import org.springframework.datastore.graph.api.*; + +import java.util.Iterator; +import java.util.List; public abstract class Neo4jHelper { @@ -16,17 +16,36 @@ public abstract class Neo4jHelper { public final static RelationshipType INSTANCE_OF_RELATIONSHIP_TYPE = DynamicRelationshipType.withName("INSTANCE_OF"); public final static String SUBREFERENCE_NODE_COUNTER_KEY = "count"; - - public static Node findSubreferenceNode(Class entityClass, GraphDatabaseService gds) { - RelationshipType subRefRelType = DynamicRelationshipType.withName("SUBREF_" + entityClass.getName()); + public static final String SUBREF_PREFIX = "SUBREF_"; + + public static Node obtainSubreferenceNode(Class entityClass, GraphDatabaseService gds) { + RelationshipType subRefRelType = DynamicRelationshipType.withName(SUBREF_PREFIX + entityClass.getName()); return new GraphDatabaseUtil(gds).getOrCreateSubReferenceNode(subRefRelType); } - + public static Node findSubreferenceNode(Class entityClass, GraphDatabaseService gds) { + RelationshipType subRefRelType = DynamicRelationshipType.withName(SUBREF_PREFIX + entityClass.getName()); + final Iterator it = gds.getReferenceNode().getRelationships(subRefRelType, Direction.OUTGOING).iterator(); + return it.hasNext() ? it.next().getEndNode() : null; + } + public static long count(Class entityClass, GraphDatabaseService gds) { Node subrefNode = findSubreferenceNode(entityClass, gds); + if (subrefNode==null) return 0; return (Integer) subrefNode.getProperty(SUBREFERENCE_NODE_COUNTER_KEY, 0); } + public static String getClassNameForShortName( GraphDatabaseService gds, String shortName) { + final Node referenceNode = gds.getReferenceNode(); + for (Relationship relationship : referenceNode.getRelationships(Direction.OUTGOING)) { + final String relationshipName = relationship.getType().name(); + if (relationshipName.endsWith(shortName) && relationshipName.startsWith(SUBREF_PREFIX)) { + return relationshipName.substring(SUBREF_PREFIX.length()); + } + } + return null; + } + + public static void cleanDb(GraphDatabaseService graphDatabaseService) { Node refNode = graphDatabaseService.getReferenceNode(); for (Node node : graphDatabaseService.getAllNodes()) { @@ -39,4 +58,11 @@ public abstract class Neo4jHelper { } } + public static void createSubreferenceNodesFor(GraphDatabaseService gds, List classNames) { + final GraphDatabaseUtil graphDatabaseUtil = new GraphDatabaseUtil(gds); + for (String className : classNames) { + RelationshipType subRefRelType = DynamicRelationshipType.withName(SUBREF_PREFIX + className); + graphDatabaseUtil.getOrCreateSubReferenceNode(subRefRelType); + } + } } diff --git a/src/main/java/org/springframework/datastore/graph/neo4j/spi/node/Neo4jNodeBacking.aj b/src/main/java/org/springframework/datastore/graph/neo4j/spi/node/Neo4jNodeBacking.aj index bee5b0435..cb27c9215 100644 --- a/src/main/java/org/springframework/datastore/graph/neo4j/spi/node/Neo4jNodeBacking.aj +++ b/src/main/java/org/springframework/datastore/graph/neo4j/spi/node/Neo4jNodeBacking.aj @@ -1,6 +1,7 @@ package org.springframework.datastore.graph.neo4j.spi.node; import java.lang.reflect.Field; +import java.util.*; import org.aspectj.lang.reflect.FieldSignature; import org.neo4j.graphdb.GraphDatabaseService; @@ -8,6 +9,7 @@ import org.neo4j.graphdb.Node; import org.neo4j.graphdb.NotInTransactionException; import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.RelationshipType; +import org.neo4j.kernel.EmbeddedGraphDatabase; import org.neo4j.util.GraphDatabaseUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.InvalidDataAccessResourceUsageException; @@ -19,6 +21,10 @@ import org.springframework.datastore.graph.neo4j.fieldaccess.FieldAccessor; import org.springframework.datastore.graph.neo4j.fieldaccess.FieldAccessorFactory; import org.springframework.persistence.support.AbstractTypeAnnotatingMixinFields; import org.springframework.persistence.support.EntityInstantiator; +import org.springframework.util.ObjectUtils; + +import javax.transaction.Status; +import javax.transaction.SystemException; /** * Aspect to turn an object annotated with GraphEntity into a graph entity using Neo4J. @@ -64,27 +70,44 @@ public aspect Neo4jNodeBacking extends AbstractTypeAnnotatingMixinFields NodeBacked.dirty; + public void NodeBacked.setUnderlyingNode(Node n) { this.underlyingNode = n; } @@ -93,21 +116,48 @@ public aspect Neo4jNodeBacking extends AbstractTypeAnnotatingMixinFields(); + this.dirty.put(f,previousValue); + } + + private Iterable> NodeBacked.eachDirty() { + return this.dirty!=null ? this.dirty.entrySet() : Collections.emptyMap().entrySet(); + } + //------------------------------------------------------------------------- // Equals and hashCode for Neo4j entities. // Final to prevent overriding. //------------------------------------------------------------------------- // TODO could use template method for further checks if needed public final boolean NodeBacked.equals(Object obj) { + if (obj == this) return true; + if (!hasUnderlyingNode()) return false; if (obj instanceof NodeBacked) { return this.getUnderlyingNode().equals(((NodeBacked) obj).getUnderlyingNode()); } @@ -115,53 +165,153 @@ public aspect Neo4jNodeBacking extends AbstractTypeAnnotatingMixinFields fieldType = f.getType(); - if (isPropertyType(fieldType)) { - String propName = FieldAccessorFactory.getNeo4jPropertyName(f); - entity.getUnderlyingNode().setProperty(propName, newVal); - log.info("SET " + f + " -> Neo4J simple node property [" + propName + "] with value=[" + newVal + "]"); - return proceed(entity, newVal); - } - - FieldAccessor accessor = fieldAccessorFactory.forField(f); - if (accessor == null) { - log.info("Ignored SET " + f + ": " + f.getType().getName() + " not primitive or GraphEntity"); - return proceed(entity, newVal); - } - log.info("SET " + f + " -> Neo4J relationship with value=[" + newVal + "]"); - Object result = accessor.apply(entity, newVal); - return proceed(entity,result); - } catch(NotInTransactionException e) { - throw new InvalidDataAccessResourceUsageException("Not in a Neo4j transaction.", e); - } + + private Object getValueFromEntity(Field field, NodeBacked entity) { + try { + field.setAccessible(true); + return field.get(entity); + } catch (IllegalAccessException e) { + throw new RuntimeException("Error accessing field "+field+" in "+entity.getClass(),e); + } + } + /* + always runs inside a transaction + */ + private ShouldProceedOrReturn getNodePropertyOrRelationship(Field field, NodeBacked entity) { + // TODO fix arrays, TODO serialize other types as byte[] or string (for indexing, querying) via Annotation + if (isIdField(field)) return new ShouldProceedOrReturn(entity.getUnderlyingNode().getId()); + if (isPropertyType(field.getType())) { + String propName = FieldAccessorFactory.getNeo4jPropertyName(field); + log.info("GET " + field + " <- Neo4J simple node property [" + propName + "]"); + Node node = entity.getUnderlyingNode(); + Object nodeProperty = node.getProperty(propName, getDefaultValue(field.getType())); + return new ShouldProceedOrReturn(nodeProperty); + } + + FieldAccessor accessor = fieldAccessorFactory.forField(field); + if (accessor!=null) { + Object obj = accessor.readObject(entity); + if (obj != null) { + return new ShouldProceedOrReturn(obj); + } + } + log.info("Ignored GET " + field + ": " + field.getType().getName() + " not primitive or GraphEntity"); + return new ShouldProceedOrReturn(); + } + + private boolean isIdField(Field field) { + if (!field.getName().equals("id")) return false; + final Class type = field.getType(); + return type.equals(Long.class) || type.equals(long.class); + } + + private Object getDefaultValue(Class type) { + if (type.isPrimitive()) { + if (type.equals(boolean.class)) return false; + return 0; + } + return null; + } + + private void flushDirty(NodeBacked entity) { + if (transactionIsRunning()) { + final boolean newNode=!entity.hasUnderlyingNode(); + if (newNode) { + createAndAssignNode(entity); + } + if (entity.isDirty()) { + for (final Map.Entry entry : entity.eachDirty()) { + final Field field = entry.getKey(); + if (!newNode) { + checkConcurrentModification(entity, entry, field); + } + setNodePropertyOrRelationship(field, entity, getValueFromEntity(field,entity)); + } + entity.clearDirty(); + } + } + } + + private void checkConcurrentModification(NodeBacked entity, Map.Entry entry, Field field) { + final Object nodeValue = getNodePropertyOrRelationship(field, entity).value; + final Object previousValue = entry.getValue(); + if (!ObjectUtils.nullSafeEquals(nodeValue,previousValue)) { + throw new ConcurrentModificationException("Node "+entity.getUnderlyingNode()+" field "+field+" changed in between previous "+ previousValue +" current "+nodeValue); // todo or just overwrite + } + } + + /* + always called inside a transaction + */ + + private ShouldProceedOrReturn setNodePropertyOrRelationship(Field field, NodeBacked entity, Object newVal) { + try { + if (isIdField(field)) return new ShouldProceedOrReturn(null); + if (isPropertyType(field.getType())) { + String propName = FieldAccessorFactory.getNeo4jPropertyName(field); + if (newVal==null) { + entity.getUnderlyingNode().removeProperty(propName); + } else { + entity.getUnderlyingNode().setProperty(propName, newVal); + } + log.info("SET " + field + " -> Neo4J simple node property [" + propName + "] with value=[" + newVal + "]"); + return new ShouldProceedOrReturn(true,newVal); + } + + FieldAccessor accessor = fieldAccessorFactory.forField(field); + if (accessor == null) { + log.info("Ignored SET " + field + ": " + field.getType().getName() + " not primitive or GraphEntity"); + return new ShouldProceedOrReturn(true,newVal); + } + log.info("SET " + field + " -> Neo4J relationship with value=[" + newVal + "]"); + Object result = accessor.apply(entity, newVal); + return new ShouldProceedOrReturn(true,result); + } catch(NotInTransactionException e) { + throw new InvalidDataAccessResourceUsageException("Not in a Neo4j transaction.", e); + } + } + Object around(NodeBacked entity): entityFieldGet(entity) { + FieldSignature fieldSignature = (FieldSignature) thisJoinPoint.getSignature(); + Field f = fieldSignature.getField(); + + if (!transactionIsRunning()) { + if (!entity.hasUnderlyingNode() || (entity.isDirty(f))) { + return proceed(entity); + } + } + + flushDirty(entity); + + ShouldProceedOrReturn shouldProceedOrReturn=getNodePropertyOrRelationship(f,entity); + if (shouldProceedOrReturn.proceed) { + return proceed(entity); + } else { + return shouldProceedOrReturn.value; + } + } + + Object around(NodeBacked entity, Object newVal) : entityFieldSet(entity, newVal) { + FieldSignature fieldSignature=(FieldSignature) thisJoinPoint.getSignature(); + Field f = fieldSignature.getField(); + if (!transactionIsRunning()) { + if (!entity.isDirty(f)) { + Object existingValue = entity.hasUnderlyingNode() ? getDefaultValue(f.getType()) : getValueFromEntity(f,entity); + entity.addDirty(f,existingValue); + } + return proceed(entity, newVal); + } + flushDirty(entity); + + ShouldProceedOrReturn shouldProceedOrReturn=setNodePropertyOrRelationship(f,entity,newVal); + if (shouldProceedOrReturn.proceed) { + return proceed(entity,shouldProceedOrReturn.value); + } else { + return shouldProceedOrReturn.value; + } } private boolean isPropertyType(Class fieldType) { diff --git a/src/main/java/org/springframework/datastore/graph/neo4j/spi/node/ShouldProceedOrReturn.java b/src/main/java/org/springframework/datastore/graph/neo4j/spi/node/ShouldProceedOrReturn.java new file mode 100644 index 000000000..6972db6a4 --- /dev/null +++ b/src/main/java/org/springframework/datastore/graph/neo4j/spi/node/ShouldProceedOrReturn.java @@ -0,0 +1,25 @@ +package org.springframework.datastore.graph.neo4j.spi.node; + +/** + * @author Michael Hunger + * @since 29.08.2010 + */ +public class ShouldProceedOrReturn { + public final boolean proceed; + public final Object value; + + public ShouldProceedOrReturn() { + this.proceed = true; + this.value = null; + } + + public ShouldProceedOrReturn(final Object value) { + this.proceed = false; + this.value = value; + } + + public ShouldProceedOrReturn(final boolean proceed, final Object value) { + this.proceed = true; + this.value = value; + } +} diff --git a/src/main/java/org/springframework/datastore/graph/neo4j/spi/relationship/Neo4jRelationshipBacking.aj b/src/main/java/org/springframework/datastore/graph/neo4j/spi/relationship/Neo4jRelationshipBacking.aj index 9dffc7754..c9ce71588 100644 --- a/src/main/java/org/springframework/datastore/graph/neo4j/spi/relationship/Neo4jRelationshipBacking.aj +++ b/src/main/java/org/springframework/datastore/graph/neo4j/spi/relationship/Neo4jRelationshipBacking.aj @@ -32,8 +32,8 @@ public aspect Neo4jRelationshipBacking extends AbstractTypeAnnotatingMixinFields private EntityInstantiator graphEntityInstantiator; @Autowired - public void init(EntityInstantiator gei) { - this.graphEntityInstantiator = gei; + public void setEntityInstantiator(EntityInstantiator entityInstantiator) { + this.graphEntityInstantiator = entityInstantiator; } // Introduced fields @@ -46,8 +46,12 @@ public aspect Neo4jRelationshipBacking extends AbstractTypeAnnotatingMixinFields public Relationship RelationshipBacked.getUnderlyingRelationship() { return underlyingRelationship; } + public boolean RelationshipBacked.hasUnderlyingRelationship() { + return underlyingRelationship!=null; + } - public long RelationshipBacked.getId() { + public Long RelationshipBacked.getId() { + if (!hasUnderlyingRelationship()) return null; return underlyingRelationship.getId(); } diff --git a/src/test/java/org/springframework/datastore/graph/neo4j/Person_Graph_Entity.aj b/src/test/java/org/springframework/datastore/graph/neo4j/Person_Graph_Entity.aj index 5ce4190c0..39e263cfe 100644 --- a/src/test/java/org/springframework/datastore/graph/neo4j/Person_Graph_Entity.aj +++ b/src/test/java/org/springframework/datastore/graph/neo4j/Person_Graph_Entity.aj @@ -3,7 +3,6 @@ package org.springframework.datastore.graph.neo4j; import java.util.ArrayList; import java.util.List; -import org.neo4j.graphdb.Direction; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Relationship; @@ -45,7 +44,7 @@ privileged aspect Person_Graph_Entity { } public static List Person.findAllPeople() { - Node subrefNode = Neo4jHelper.findSubreferenceNode(Person.class, graphDatabaseService()); + Node subrefNode = Neo4jHelper.obtainSubreferenceNode(Person.class, graphDatabaseService()); // TODO Neo4j should add lazy list on top of graph List people = new ArrayList((int) countPeople()); for (Relationship rel : subrefNode.getRelationships(Neo4jHelper.INSTANCE_OF_RELATIONSHIP_TYPE, org.neo4j.graphdb.Direction.INCOMING)) { diff --git a/src/test/java/org/springframework/datastore/graph/neo4j/jpa/Neo4jEntityManagerTest.java b/src/test/java/org/springframework/datastore/graph/neo4j/jpa/Neo4jEntityManagerTest.java index 3958d32ea..5fdd65173 100644 --- a/src/test/java/org/springframework/datastore/graph/neo4j/jpa/Neo4jEntityManagerTest.java +++ b/src/test/java/org/springframework/datastore/graph/neo4j/jpa/Neo4jEntityManagerTest.java @@ -1,5 +1,6 @@ package org.springframework.datastore.graph.neo4j.jpa; +import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -9,11 +10,11 @@ import javax.annotation.Resource; import javax.persistence.EntityManager; import javax.persistence.EntityTransaction; import javax.persistence.PersistenceContext; +import javax.persistence.Query; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; +import org.hamcrest.CoreMatchers; +import org.hamcrest.Matcher; +import org.junit.*; import org.junit.runner.RunWith; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Node; @@ -24,6 +25,10 @@ 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.Collections; +import java.util.HashSet; + /** * @author Michael Hunger * @since 20.08.2010 @@ -43,8 +48,8 @@ public class Neo4jEntityManagerTest { @Before public void cleanDb() { Neo4jHelper.cleanDb(graphDatabaseService); - node = graphDatabaseService.createNode(); - person = new Person(node); + person = new Person("Michael",35); + node = person.getUnderlyingNode(); } @Test @@ -70,6 +75,45 @@ public class Neo4jEntityManagerTest { final Person found = entityManager.find(Person.class, node.getId()); assertEquals(person,found); } + @Test + public void testFindSingle() throws Exception { + final Query query = entityManager.createQuery("select o from Person o"); + final Person found = (Person) query.getSingleResult(); + assertEquals(person,found); + } + @Test + public void testFindAll() throws Exception { + final Query query = entityManager.createQuery("select o from Person o"); + Collection people=query.getResultList(); + Assert.assertEquals(asList(person),people); + } + + @Test + public void testFindAll2() throws Exception { + final Person person2 = new Person("Rod", 39); + final Query query = entityManager.createQuery("select o from Person o"); + Collection people=query.getResultList(); + Assert.assertEquals(new HashSet(asList(person,person2)),new HashSet(people)); + } + @Test + public void testFindAllStart() throws Exception { + final Query query = entityManager.createQuery("select o from Person o").setFirstResult(1); + Collection people=query.getResultList(); + Assert.assertEquals(Collections.emptySet(),new HashSet(people)); + } + @Test + public void testFindAllEnd() throws Exception { + final Query query = entityManager.createQuery("select o from Person o").setMaxResults(0); + Collection people=query.getResultList(); + Assert.assertEquals(Collections.emptySet(),new HashSet(people)); + } + @Test + public void testFindAllStartEnd() throws Exception { + new Person("Rod", 39); + final Query query = entityManager.createQuery("select o from Person o").setMaxResults(1).setFirstResult(1); + Collection people=query.getResultList(); + Assert.assertEquals(1,people.size()); + } @Test public void testGetReference() throws Exception { 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 2738ea66e..8ddb3dcac 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 @@ -134,12 +134,13 @@ public class Neo4jGraphPersistenceTest { Assert.assertEquals(boss, p.getBoss()); } - @Test(expected = InvalidDataAccessResourceUsageException.class) + // @Test(expected = InvalidDataAccessResourceUsageException.class) public void testCreateOutsideTransaction() { Person p = new Person("Michael", 35); + Assert.assertEquals(35,p.getAge()); } - @Test(expected = InvalidDataAccessResourceUsageException.class) + // @Test(expected = InvalidDataAccessResourceUsageException.class) public void testSetPropertyOutsideTransaction() { Transaction tx = graphDatabaseService.beginTx(); Person p = null; @@ -150,9 +151,19 @@ public class Neo4jGraphPersistenceTest { tx.finish(); } p.setAge(25); + Assert.assertEquals(25,p.getAge()); + tx = graphDatabaseService.beginTx(); + try { + Assert.assertEquals(25,p.getAge()); + p.setAge(20); + tx.success(); + } finally { + tx.finish(); + } + Assert.assertEquals(20,p.getAge()); } - @Test(expected = InvalidDataAccessResourceUsageException.class) + // @Test(expected = InvalidDataAccessResourceUsageException.class) public void testCreateRelationshipOutsideTransaction() { Transaction tx = graphDatabaseService.beginTx(); Person p = null; @@ -165,6 +176,17 @@ public class Neo4jGraphPersistenceTest { tx.finish(); } p.setSpouse(spouse); + Assert.assertEquals(spouse,p.getSpouse()); + Person spouse2; + tx = graphDatabaseService.beginTx(); + try { + Assert.assertEquals(spouse,p.getSpouse()); + spouse2 = new Person("Rana", 5); + tx.success(); + } finally { + tx.finish(); + } + Assert.assertEquals(spouse2,p.getSpouse()); } @Test @@ -179,7 +201,14 @@ public class Neo4jGraphPersistenceTest { } Assert.assertEquals("Wrong age.", (int)35, (int)p.getAge()); } - + + @Test + public void testFindOutsideTransaction() { + final FinderFactory factory = new FinderFactory(graphDatabaseService, graphEntityInstantiator); + final Finder finder = factory.getFinderForClass(Person.class); + Assert.assertEquals(false,finder.findAll().iterator().hasNext()); + } + @Test(expected = InvalidDataAccessApiUsageException.class) @Transactional public void testCircularRelationship() { diff --git a/src/test/resources/META-INF/persistence.xml b/src/test/resources/META-INF/persistence.xml index 4a5564961..98be20d92 100644 --- a/src/test/resources/META-INF/persistence.xml +++ b/src/test/resources/META-INF/persistence.xml @@ -3,6 +3,9 @@ org.springframework.datastore.graph.neo4j.jpa.Neo4jPersistenceProvider + org.springframework.datastore.graph.neo4j.Person + org.springframework.datastore.graph.neo4j.Group + org.springframework.datastore.graph.neo4j.Friendship diff --git a/src/test/resources/org/springframework/datastore/graph/neo4j/spi/Neo4jGraphPersistenceTest-context.xml b/src/test/resources/org/springframework/datastore/graph/neo4j/spi/Neo4jGraphPersistenceTest-context.xml index c6d2a5994..efc91107c 100644 --- a/src/test/resources/org/springframework/datastore/graph/neo4j/spi/Neo4jGraphPersistenceTest-context.xml +++ b/src/test/resources/org/springframework/datastore/graph/neo4j/spi/Neo4jGraphPersistenceTest-context.xml @@ -71,7 +71,9 @@ - + + +