diff --git a/.settings/org.eclipse.ajdt.core.prefs b/.settings/org.eclipse.ajdt.core.prefs new file mode 100644 index 000000000..94d66a7f4 --- /dev/null +++ b/.settings/org.eclipse.ajdt.core.prefs @@ -0,0 +1,42 @@ +#Mon Mar 29 14:00:24 PDT 2010 +eclipse.preferences.version=1 +org.aspectj.ajdt.core.compiler.BuildOptions.showweavemessages=false +org.aspectj.ajdt.core.compiler.lint.BrokeSerialVersionCompatibility=ignore +org.aspectj.ajdt.core.compiler.lint.CannotImplementLazyTJP=ignore +org.aspectj.ajdt.core.compiler.lint.InvalidAbsoluteTypeName=warning +org.aspectj.ajdt.core.compiler.lint.NeedSerialVersionUIDField=ignore +org.aspectj.ajdt.core.compiler.lint.NoInterfaceCtorJoinpoint=warning +org.aspectj.ajdt.core.compiler.lint.ShadowNotInStructure=ignore +org.aspectj.ajdt.core.compiler.lint.TypeNotExposedToWeaver=warning +org.aspectj.ajdt.core.compiler.lint.UnresolvableMember=warning +org.aspectj.ajdt.core.compiler.lint.WildcardTypeName=ignore +org.aspectj.ajdt.core.compiler.lint.adviceDidNotMatch=warning +org.aspectj.ajdt.core.compiler.lint.annotationAsTargetForDecpIgnored=warning +org.aspectj.ajdt.core.compiler.lint.calculatingSerialVersionUID=ignore +org.aspectj.ajdt.core.compiler.lint.cantFindType=error +org.aspectj.ajdt.core.compiler.lint.cantFindTypeAffectingJPMatch=warning +org.aspectj.ajdt.core.compiler.lint.cantMatchArrayTypeOnVarargs=ignore +org.aspectj.ajdt.core.compiler.lint.elementAlreadyAnnotated=warning +org.aspectj.ajdt.core.compiler.lint.enumAsTargetForDecpIgnored=warning +org.aspectj.ajdt.core.compiler.lint.invalidTargetForAnnotation=warning +org.aspectj.ajdt.core.compiler.lint.multipleAdviceStoppingLazyTjp=ignore +org.aspectj.ajdt.core.compiler.lint.noExplicitConstructorCall=warning +org.aspectj.ajdt.core.compiler.lint.noGuardForLazyTjp=ignore +org.aspectj.ajdt.core.compiler.lint.noJoinpointsForBridgeMethods=warning +org.aspectj.ajdt.core.compiler.lint.runtimeExceptionNotSoftened=warning +org.aspectj.ajdt.core.compiler.lint.swallowedExceptionInCatchBlock=ignore +org.aspectj.ajdt.core.compiler.lint.uncheckedAdviceConversion=warning +org.aspectj.ajdt.core.compiler.lint.uncheckedArgument=warning +org.aspectj.ajdt.core.compiler.lint.unmatchedTargetKind=warning +org.aspectj.ajdt.core.compiler.lint.unorderedAdviceAtShadow=ignore +org.aspectj.ajdt.core.compiler.list.UnmatchedSuperTypeInCall=warning +org.aspectj.ajdt.core.compiler.weaver.XHasMember=false +org.aspectj.ajdt.core.compiler.weaver.XNoInline=false +org.aspectj.ajdt.core.compiler.weaver.XNotReweavable=false +org.aspectj.ajdt.core.compiler.weaver.XSerializableAspects=false +org.aspectj.ajdt.core.compiler.weaver.timers=false +org.aspectj.ajdt.core.compiler.weaver.verbose=false +org.aspectj.ajdt.core.complier.lint.aspectExcludedByConfiguration=ignore +org.eclipse.ajdt.core.builder.incrementalCompilationOptimizations=true +org.eclipse.ajdt.core.compiler.nonStandardOptions=-Xset\:pipelineCompilation\=false -XhasMember +org.eclipse.ajdt.core.compiler.useProjectSettings=true diff --git a/src/main/java/org/springframework/persistence/graph/neo4j/Neo4jNodeBacking.aj b/src/main/java/org/springframework/persistence/graph/neo4j/Neo4jNodeBacking.aj new file mode 100644 index 000000000..86b3add02 --- /dev/null +++ b/src/main/java/org/springframework/persistence/graph/neo4j/Neo4jNodeBacking.aj @@ -0,0 +1,189 @@ +package org.springframework.persistence.graph.neo4j; + +import java.lang.reflect.Field; + +import org.aspectj.lang.Signature; +import org.aspectj.lang.reflect.FieldSignature; +import org.neo4j.graphdb.DynamicRelationshipType; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.RelationshipType; +import org.neo4j.util.GraphDatabaseUtil; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.persistence.graph.Direction; +import org.springframework.persistence.graph.GraphEntity; +import org.springframework.persistence.graph.Relationship; +import org.springframework.persistence.support.AbstractTypeAnnotatingMixinFields; +import org.springframework.persistence.support.EntityInstantiator; + +/** + * Aspect to turn an object annotated with GraphEntity into a graph entity using Neo4J. + * Delegates all field access (except for fields assumed to be transient) + * to an underlying Neo4 graph node. + * + * @author Rod Johnson + */ +public aspect Neo4jNodeBacking extends AbstractTypeAnnotatingMixinFields { + + //------------------------------------------------------------------------- + // Configure aspect for whole system. + // init() method can be invoked automatically if the aspect is a Spring + // bean, or called in user code. + //------------------------------------------------------------------------- + // Aspect shared Neo4J Graph Database Service + private GraphDatabaseService graphDatabaseService; + + private EntityInstantiator graphEntityInstantiator; + + private GraphDatabaseUtil graphDatabaseUtil; + + @Autowired + public void init(GraphDatabaseService gds, EntityInstantiator gei) { + this.graphDatabaseService = gds; + this.graphEntityInstantiator = gei; + this.graphDatabaseUtil = new GraphDatabaseUtil(gds); + } + + + //------------------------------------------------------------------------- + // Advise user-defined constructors of NodeBacked objects to create a new + // Neo4J backing node + //------------------------------------------------------------------------- + pointcut arbitraryUserConstructorOfNodeBackedObject(NodeBacked entity) : + execution((@GraphEntity *).new(..)) && + !execution((@GraphEntity *).new(Node)) && + this(entity); + + + // Create a new node in the Graph if no Node was passed in a constructor + before(NodeBacked entity) : arbitraryUserConstructorOfNodeBackedObject(entity) { + entity.setUnderlyingNode(graphDatabaseService.createNode()); + log.info("User-defined constructor called on class " + entity.getClass() + "; created Node [" + entity.getUnderlyingNode() +"]; " + + "Updating metamodel"); + // TODO pull naming out into a strategy interface + Node subReference = Neo4jHelper.findSubreferenceNode(entity.getClass(), graphDatabaseService); + entity.getUnderlyingNode().createRelationshipTo(subReference, Neo4jHelper.INSTANCE_OF_RELATIONSHIP_TYPE); + graphDatabaseUtil.incrementAndGetCounter(subReference, Neo4jHelper.SUBREFERENCE_NODE_COUNTER_KEY); + } + + + // Introduced field + private Node NodeBacked.underlyingNode; + + public void NodeBacked.setUnderlyingNode(Node n) { + this.underlyingNode = n; + } + + public Node NodeBacked.getUnderlyingNode() { + return underlyingNode; + } + + + //------------------------------------------------------------------------- + // 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 instanceof NodeBacked) { + return this.getUnderlyingNode().equals(((NodeBacked) obj).getUnderlyingNode()); + } + return false; + } + + public final int NodeBacked.hashCode() { + return getUnderlyingNode().hashCode(); + } + + + Object around(NodeBacked entity) : entityFieldGet(entity) { + Field f = ((FieldSignature) thisJoinPoint.getSignature()).getField(); + + // TODO fix arrays + if (f.getType().isPrimitive() || f.getType().equals(String.class)) { + String propName = getNeo4jPropertyName(thisJoinPoint.getSignature()); + log.info("GET " + f + " <- Neo4J simple node property [" + propName + "]"); + return entity.getUnderlyingNode().getProperty(propName, null); + } + + // Look for a relationship + if (isNeo4jRelationshipField(f)) { + Relationship r = f.getAnnotation(Relationship.class); + if (r == null) { + throw new IllegalStateException("Must have @Relationship on " + f); + } + Node me = entity.getUnderlyingNode(); + if (me == null) { + throw new IllegalStateException("Entity must have a backing Node"); + } + RelationshipType type = DynamicRelationshipType.withName(r.type()); + org.neo4j.graphdb.Relationship singleRelationship = me.getSingleRelationship(type, r.direction().toNeo4jDir()); + + // TODO is this correct + if (singleRelationship == null) { + log.info("GET " + f + ": " + f.getType().getName() + ": not set yet so returning field value"); + return proceed(entity); + } + Node targetNode = singleRelationship.getOtherNode(me); + log.info("GET " + f + ": " + f.getType().getName() + ": setting from relationship " + singleRelationship); + return graphEntityInstantiator.createEntityFromState(targetNode, (Class) f.getType()); + } + + // Must be transient + log.info("Ignored GET " + f + ": " + f.getType().getName() + " not primitive or GraphEntity"); + return proceed(entity); + } + + + Object around(NodeBacked entity, Object newVal) : entityFieldSet(entity, newVal) { + Field f = ((FieldSignature) thisJoinPoint.getSignature()).getField(); + + // TODO fix arrays + if (f.getType().isPrimitive() || f.getType().equals(String.class)) { + String propName = getNeo4jPropertyName(thisJoinPoint.getSignature()); + entity.getUnderlyingNode().setProperty(propName, newVal); + log.info("SET " + f + " -> Neo4J simple node property [" + propName + "] with value=[" + newVal + "]"); + return null; + } + + // Look for a relationship + if (isNeo4jRelationshipField(f)) { + Relationship r = f.getAnnotation(Relationship.class); + if (r == null) { + throw new IllegalStateException("Must have @Relationship on " + f); + } + graphEntityFieldSet(entity, r, (NodeBacked) newVal); + log.info("SET " + f + " -> Neo4J relationship with value=[" + newVal + "]"); + return null; + } + else { + log.info("Ignored SET " + f + ": " + f.getType().getName() + " not primitive or GraphEntity"); + return proceed(entity, newVal); + } + } + + + private void graphEntityFieldSet(NodeBacked entity, Relationship r, NodeBacked newVal) { + Node me = entity.getUnderlyingNode(); + RelationshipType type = DynamicRelationshipType.withName(r.type()); + Node targetNode = newVal.getUnderlyingNode(); + if (r.direction() == Direction.OUTGOING) { + me.createRelationshipTo(targetNode, type); + } + else { + targetNode.createRelationshipTo(me, type); + } + } + + private boolean isNeo4jRelationshipField(Field f) { + //return f.getType().isAnnotationPresent(GraphEntity.class); + return NodeBacked.class.isAssignableFrom(f.getType()); + } + + + private String getNeo4jPropertyName(Signature sig) { + return sig.toShortString(); + } + +} diff --git a/src/main/java/org/springframework/persistence/support/AbstractConstructorEntityInstantiator.java b/src/main/java/org/springframework/persistence/support/AbstractConstructorEntityInstantiator.java new file mode 100644 index 000000000..9d7efbc10 --- /dev/null +++ b/src/main/java/org/springframework/persistence/support/AbstractConstructorEntityInstantiator.java @@ -0,0 +1,61 @@ +package org.springframework.persistence.support; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.ClassUtils; + +/** + * Try for a constructor taking state: failing that, try a no-arg + * constructor and then setUnderlyingNode(). + * + * @author Rod Johnson + */ +public abstract class AbstractConstructorEntityInstantiator implements EntityInstantiator { + + private final Log log = LogFactory.getLog(getClass()); + + final public T createEntityFromState(STATE n, Class c) { + try { + return fromStateInternal(n, c); + } catch (InstantiationException e) { + throw new IllegalArgumentException(e); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException(e); + } catch (InvocationTargetException e) { + throw new IllegalArgumentException(e); + } + } + + final private T fromStateInternal(STATE n, Class c) throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException { + // TODO this is fragile + Class stateInterface = (Class) n.getClass().getInterfaces()[0]; + Constructor nodeConstructor = ClassUtils.getConstructorIfAvailable(c, stateInterface); + if (nodeConstructor != null) { + // TODO is this the correct way to instantiate or does Spring have a preferred way? + log.info("Using " + c + " constructor taking " + stateInterface); + return nodeConstructor.newInstance(n); + } + + Constructor noArgConstructor = ClassUtils.getConstructorIfAvailable(c); + if (noArgConstructor != null) { + log.info("Using " + c + " no-arg constructor"); + T t = noArgConstructor.newInstance(); + setState(t, n); + return t; + } + + throw new IllegalArgumentException(getClass().getSimpleName() + ": entity " + c + " must have either a constructor taking [" + stateInterface + + "] or a no-arg constructor and state set method"); + } + + /** + * Subclasses must implement to set state + * @param entity + * @param s + */ + protected abstract void setState(BACKING_INTERFACE entity, STATE s); + +} diff --git a/src/main/java/org/springframework/persistence/support/AbstractMixinFields.aj b/src/main/java/org/springframework/persistence/support/AbstractMixinFields.aj new file mode 100644 index 000000000..9e6473ea3 --- /dev/null +++ b/src/main/java/org/springframework/persistence/support/AbstractMixinFields.aj @@ -0,0 +1,54 @@ +package org.springframework.persistence.support; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Configurable; +import org.springframework.persistence.RelatedEntity; + +/** + * Abstract superaspect to advise field read and write + * and introduce a mixin interface. + * + * @param type of introduced interface + * + * @author Rod Johnson + */ +privileged abstract public aspect AbstractMixinFields { + + protected final Log log = LogFactory.getLog(getClass()); + + //------------------------------------------------------------------------- + // ITDs to add behavior and state to classes + //------------------------------------------------------------------------- + // Enable Spring DI for all mixed-in objects + declare @type: N+: @Configurable; + + //------------------------------------------------------------------------- + // Advice for field get/set to delegate to backing Node. + //------------------------------------------------------------------------- + protected pointcut entityFieldGet(N entity) : + get(* N+.*) && + this(entity) && + !(get(@RelatedEntity * *) + || get(* N.*) + || getsNotToAdvise()); + + /** + * Never matches. Subclasses can override to exempt certain field reads from advice + */ + protected pointcut getsNotToAdvise(); + + protected pointcut entityFieldSet(N entity, Object newVal) : + set(* N+.*) && + this(entity) && + args(newVal) && + !(set(@RelatedEntity * *) || + set(* N.*) || + setsNotToAdvise()); + + /** + * Never matches. Subclasses can override to exempt certain field writes from advice + */ + protected pointcut setsNotToAdvise(); + +} diff --git a/src/main/java/org/springframework/persistence/support/AbstractTypeAnnotatingMixinFields.aj b/src/main/java/org/springframework/persistence/support/AbstractTypeAnnotatingMixinFields.aj new file mode 100644 index 000000000..ba63e81e4 --- /dev/null +++ b/src/main/java/org/springframework/persistence/support/AbstractTypeAnnotatingMixinFields.aj @@ -0,0 +1,21 @@ +package org.springframework.persistence.support; + +import java.lang.annotation.Annotation; + +/** + * Abstract superaspect for aspects that advice + * field access with a mixin for all types annotated with + * a given annotation. + * + * @param annotation on entity + * @param type of introduced interface + * + * @author Rod Johnson + */ +privileged abstract public aspect AbstractTypeAnnotatingMixinFields + extends AbstractMixinFields { + + // ITD to introduce N state to Annotated objects + declare parents : (@ET *) implements N; + +} diff --git a/src/main/java/org/springframework/persistence/support/EntityInstantiator.java b/src/main/java/org/springframework/persistence/support/EntityInstantiator.java new file mode 100644 index 000000000..f17db133e --- /dev/null +++ b/src/main/java/org/springframework/persistence/support/EntityInstantiator.java @@ -0,0 +1,32 @@ +package org.springframework.persistence.support; + + +/** + * Interface to be implemented by classes that can instantiate and + * configure entities. + * The framework must do this when creating objects resulting from finders, + * even when there may be no no-arg constructor supplied by the user. + * + * @author Rod Johnson + */ +public interface EntityInstantiator { + + /* + * The best solution if available is to add a constructor that takes Node + * to each GraphEntity. This means generating an aspect beside every + * class as Roo presently does. + * + * An alternative that does not require Roo + * is a user-authored constructor taking Node and calling setUnderlyingNode() + * but this is less elegant and pollutes the domain object. + * + * If the user supplies a no-arg constructor, instantiation can occur by invoking it + * prior to calling setUnderlyingNode(). + * + * If the user does NOT supply a no-arg constructor, we must rely on Sun-specific + * code to instantiate entities without invoking a constructor. + */ + + T createEntityFromState(STATE s, Class c); + +}