diff --git a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/Group.java b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/Group.java index 722d31f89..8f06f5f03 100644 --- a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/Group.java +++ b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/Group.java @@ -64,6 +64,8 @@ public class Group { @Query("start n=node({self}) match n-[:persons]->() return count(*)") private Long memberCount; + @RelatedToVia(type="mentors", direction = Direction.INCOMING) + Mentorship mentorship; @GraphProperty private String unindexedName; @@ -275,4 +277,12 @@ public class Group { public void setRolesIterable(Iterable rolesIterable) { this.rolesIterable = rolesIterable; } + + public Mentorship getMentorship() { + return mentorship; + } + + public void setMentorship(Mentorship mentorship) { + this.mentorship = mentorship; + } } diff --git a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/Mentorship.java b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/Mentorship.java new file mode 100644 index 000000000..5483cab05 --- /dev/null +++ b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/Mentorship.java @@ -0,0 +1,55 @@ +/** + * 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.neo4j.aspects; + + +import org.springframework.data.neo4j.annotation.EndNode; +import org.springframework.data.neo4j.annotation.GraphId; +import org.springframework.data.neo4j.annotation.RelationshipEntity; +import org.springframework.data.neo4j.annotation.StartNode; + +@RelationshipEntity(type = "mentors") +public class Mentorship { + + @GraphId Long id; + + public Long getId() { + return id; + } + + public Mentorship() { + } + + public Mentorship(Person mentor, Group group) { + this.mentor = mentor; + this.group = group; + } + + @StartNode + private Person mentor; + + @EndNode + private Group group; + + public Person getMentor() { + return mentor; + } + + public Group getGroup() { + return group; + } +} diff --git a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/NodeEntityRelationshipTest.java b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/NodeEntityRelationshipTest.java index 747fea40b..ec9219e20 100644 --- a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/NodeEntityRelationshipTest.java +++ b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/NodeEntityRelationshipTest.java @@ -27,14 +27,23 @@ import org.neo4j.helpers.collection.IteratorUtil; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.neo4j.aspects.Friendship; import org.springframework.data.neo4j.aspects.Group; +import org.springframework.data.neo4j.aspects.Mentorship; import org.springframework.data.neo4j.aspects.Person; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.springframework.data.neo4j.aspects.Person.persistedPerson; @@ -254,4 +263,51 @@ public class NodeEntityRelationshipTest extends EntityTestBase { group.setReadOnlyPersons(new HashSet()); } + @Test + @Transactional + public void testSingleRelatedToViaField() { + Group group = persist(new Group()); + Person mentor = persist(new Person()); + group.setMentorship(new Mentorship(mentor,group)); + persist(group); + final Node node = neo4jTemplate.getPersistentState(group); + assertEquals(1,IteratorUtil.count(node.getRelationships(Direction.INCOMING,DynamicRelationshipType.withName("mentors")))); + final Group loaded = neo4jTemplate.load(node, Group.class); + assertEquals(group.getMentorship(),loaded.getMentorship()); + assertEquals(group.getMentorship().getId(),loaded.getMentorship().getId()); + assertEquals(mentor, group.getMentorship().getMentor()); + assertEquals(group, group.getMentorship().getGroup()); + } + + @Test + @Transactional + public void testRemoveSingleRelatedToViaField() { + Group group = persist(new Group()); + Person mentor = persist(new Person()); + group.setMentorship(new Mentorship(mentor,group)); + persist(group); + group.setMentorship(null); + persist(group); + final Node node = neo4jTemplate.getPersistentState(group); + assertEquals(0,IteratorUtil.count(node.getRelationships(Direction.INCOMING,DynamicRelationshipType.withName("mentors")))); + final Group loaded = neo4jTemplate.load(node, Group.class); + assertThat(loaded.getMentorship(), is(nullValue())); + } + @Test + @Transactional + public void testUpdateSingleRelatedToViaField() { + Group group = persist(new Group()); + group.setMentorship(new Mentorship(persist(new Person()),group)); + persist(group); + final Long firstMentorshipId = group.getMentorship().getId(); + final Person mentor2 = new Person(); + group.setMentorship(new Mentorship(persist(mentor2),group)); + persist(group); + final Node node = neo4jTemplate.getPersistentState(group); + assertEquals(1,IteratorUtil.count(node.getRelationships(Direction.INCOMING,DynamicRelationshipType.withName("mentors")))); + final Group loaded = neo4jTemplate.load(node, Group.class); + assertFalse(loaded.getMentorship().getId().equals(firstMentorshipId)); + assertEquals(mentor2, group.getMentorship().getMentor()); + assertEquals(group, group.getMentorship().getGroup()); + } } diff --git a/spring-data-neo4j-cross-store/src/main/java/org/springframework/data/neo4j/cross_store/support/node/CrossStoreNodeEntityState.java b/spring-data-neo4j-cross-store/src/main/java/org/springframework/data/neo4j/cross_store/support/node/CrossStoreNodeEntityState.java index e1fb49dd9..66468990d 100644 --- a/spring-data-neo4j-cross-store/src/main/java/org/springframework/data/neo4j/cross_store/support/node/CrossStoreNodeEntityState.java +++ b/spring-data-neo4j-cross-store/src/main/java/org/springframework/data/neo4j/cross_store/support/node/CrossStoreNodeEntityState.java @@ -146,15 +146,16 @@ public class CrossStoreNodeEntityState extends Defaul new QueryFieldAccessorFactory(template), newPropertyFieldAccessorFactory(), newConvertingNodePropertyFieldAccessorFactory(), - new SingleRelationshipFieldAccessorFactory(getTemplate()) { + new RelatedToSingleFieldAccessorFactory(getTemplate()) { @Override public boolean accept(Neo4jPersistentProperty property) { return property.isAnnotationPresent(RelatedTo.class) && super.accept(property); } }, - new OneToNRelationshipFieldAccessorFactory(getTemplate()), - new ReadOnlyOneToNRelationshipFieldAccessorFactory(getTemplate()), - new OneToNRelationshipEntityFieldAccessorFactory(getTemplate()) + new RelatedToCollectionFieldAccessorFactory(getTemplate()), + new ReadOnlyRelatedToCollectionFieldAccessorFactory(getTemplate()), + new RelatedToViaSingleFieldAccessorFactory(getTemplate()), + new RelatedToViaCollectionFieldAccessorFactory(getTemplate()) ); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/AbstractNodeRelationshipFieldAccessor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/AbstractNodeRelationshipFieldAccessor.java deleted file mode 100644 index 8db12bb53..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/AbstractNodeRelationshipFieldAccessor.java +++ /dev/null @@ -1,143 +0,0 @@ -/** - * 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.neo4j.fieldaccess; - -import org.neo4j.graphdb.*; -import org.springframework.dao.InvalidDataAccessApiUsageException; - -import org.springframework.data.neo4j.mapping.MappingPolicy; -import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty; -import org.springframework.data.neo4j.support.Neo4jTemplate; -import org.springframework.util.Assert; - -import java.util.HashSet; -import java.util.Set; - -/** - * @author Michael Hunger - * @since 11.09.2010 - */ -public abstract class AbstractNodeRelationshipFieldAccessor implements FieldAccessor { - protected final RelationshipType type; - protected final Neo4jPersistentProperty property; - protected final Direction direction; - protected final Class relatedType; - protected final Neo4jTemplate template; - - public AbstractNodeRelationshipFieldAccessor(Class clazz, Neo4jTemplate template, Direction direction, RelationshipType type, Neo4jPersistentProperty property) { - this.relatedType = clazz; - this.template = template; - this.direction = direction; - this.type = type; - this.property = property; - } - - protected MappingPolicy updateMappingPolicy(MappingPolicy mappingPolicy) { - if (mappingPolicy !=null) return mappingPolicy; - return property.getMappingPolicy(); - } - - @Override - public boolean isWriteable(Object entity) { - return true; - } - - protected STATE checkUnderlyingState(Object entity) { - if (entity==null) throw new IllegalStateException("Entity is null"); - STATE node = getState(entity); - if (node != null) return node; - throw new IllegalStateException("Entity must have a backing Node"); - } - - protected void removeMissingRelationships(Node node, Set targetNodes) { - for ( Relationship relationship : node.getRelationships(type, direction) ) { - if (!targetNodes.remove(relationship.getOtherNode(node))) - relationship.delete(); - } - } - - protected void createAddedRelationships(Node node, Set targetNodes) { - for (Node targetNode : targetNodes) { - createSingleRelationship(node,targetNode); - } - } - // adding cascade - @SuppressWarnings("unchecked") - protected Set createSetOfTargetNodes(Object newVal) { - if (!(newVal instanceof Set)) { - throw new IllegalArgumentException("New value must be a Set, was: " + newVal.getClass()); - } - Set nodes=new HashSet(); - for (Object value : (Set) newVal) { - if (!relatedType.isInstance(value)) { - throw new IllegalArgumentException("New value elements must be "+relatedType); - } - nodes.add((Node)getOrCreateState(value)); - } - return nodes; - } - - protected STATE getOrCreateState(Object value) { - final STATE state = getState(value); - if (state != null) return state; - final Object saved = template.save(value); - final STATE newState = getState(saved); - Assert.notNull(newState); - return newState; - } - - protected ManagedFieldAccessorSet createManagedSet(Object entity, Set result, MappingPolicy mappingPolicy) { - return new ManagedFieldAccessorSet(entity, result, property, template,this, mappingPolicy); - } - - protected Set createEntitySetFromRelationshipEndNodes(Object entity, final MappingPolicy mappingPolicy) { - final Iterable nodes = getStatesFromEntity(entity); - final Set result = new HashSet(); - for (final TSTATE otherNode : nodes) { - Object target= template.createEntityFromState(otherNode, relatedType, mappingPolicy); - result.add(target); - } - return result; - } - - - @SuppressWarnings("unchecked") - protected void createSingleRelationship(Node start, Node end) { - if (end==null) return; - switch(direction) { - case OUTGOING : - case BOTH : { // TODO both should actually check in both directions, perhaps have the obtain method get the direction instead and figure out what to do itself - obtainSingleRelationship(start, end); - break; - } - case INCOMING : - obtainSingleRelationship(end, start); - break; - default : throw new InvalidDataAccessApiUsageException("invalid direction " + direction); - } - } - - public Object getDefaultValue() { - return null; - } - - protected abstract Relationship obtainSingleRelationship(Node start, Node end); - - protected abstract Iterable getStatesFromEntity(Object entity); - - protected abstract STATE getState(Object entity); -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/GraphBackedEntityIterableWrapper.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/GraphBackedEntityIterableWrapper.java index 3b93c5e1e..5ae62f3d5 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/GraphBackedEntityIterableWrapper.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/GraphBackedEntityIterableWrapper.java @@ -17,6 +17,7 @@ package org.springframework.data.neo4j.fieldaccess; import org.neo4j.graphdb.PropertyContainer; import org.neo4j.helpers.collection.IterableWrapper; +import org.springframework.data.neo4j.mapping.MappingPolicy; import org.springframework.data.neo4j.support.Neo4jTemplate; /** @@ -26,16 +27,18 @@ import org.springframework.data.neo4j.support.Neo4jTemplate; public class GraphBackedEntityIterableWrapper extends IterableWrapper { private final Class targetType; private final Neo4jTemplate template; + private final MappingPolicy mappingPolicy; public GraphBackedEntityIterableWrapper(Iterable iterable, Class targetType, final Neo4jTemplate template) { super(iterable); this.targetType = targetType; this.template = template; + mappingPolicy = this.template.getMappingPolicy(this.targetType); } @Override protected ENTITY underlyingObjectToObject(STATE s) { - return template.createEntityFromState(s, targetType, template.getMappingPolicy(targetType)); + return template.createEntityFromState(s, targetType, mappingPolicy); } public static GraphBackedEntityIterableWrapper create( diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/ManagedFieldAccessorSet.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/ManagedFieldAccessorSet.java index fe52dfdc8..5326db515 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/ManagedFieldAccessorSet.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/ManagedFieldAccessorSet.java @@ -49,7 +49,11 @@ public class ManagedFieldAccessorSet extends AbstractSet { this.mappingPolicy = mappingPolicy; } - @Override + public static ManagedFieldAccessorSet create(Object entity, Set result, MappingPolicy mappingPolicy, final Neo4jPersistentProperty property, final Neo4jTemplate template, final FieldAccessor fieldAccessor) { + return new ManagedFieldAccessorSet(entity, result, property, template, fieldAccessor, mappingPolicy); + } + + @Override public Iterator iterator() { final Iterator iterator = delegate.iterator(); return new Iterator() { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/NodeDelegatingFieldAccessorFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/NodeDelegatingFieldAccessorFactory.java index b3d85c3c2..73bdd4063 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/NodeDelegatingFieldAccessorFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/NodeDelegatingFieldAccessorFactory.java @@ -52,10 +52,11 @@ public class NodeDelegatingFieldAccessorFactory extends DelegatingFieldAccessorF new QueryFieldAccessorFactory(template), new PropertyFieldAccessorFactory(template), new ConvertingNodePropertyFieldAccessorFactory(template), - new SingleRelationshipFieldAccessorFactory(template), - new OneToNRelationshipFieldAccessorFactory(template), - new ReadOnlyOneToNRelationshipFieldAccessorFactory(template), - new OneToNRelationshipEntityFieldAccessorFactory(template), + new RelatedToSingleFieldAccessorFactory(template), + new RelatedToCollectionFieldAccessorFactory(template), + new ReadOnlyRelatedToCollectionFieldAccessorFactory(template), + new RelatedToViaCollectionFieldAccessorFactory(template), + new RelatedToViaSingleFieldAccessorFactory(template), new DynamicPropertiesFieldAccessorFactory(template) ); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/NodeRelationshipFieldAccessorFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/NodeRelationshipFieldAccessorFactory.java deleted file mode 100644 index d7c7360fc..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/NodeRelationshipFieldAccessorFactory.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * 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.neo4j.fieldaccess; - -import org.springframework.data.neo4j.support.Neo4jTemplate; - -/** - * @author Michael Hunger - * @since 12.09.2010 - */ -public abstract class NodeRelationshipFieldAccessorFactory implements FieldAccessorFactory { - - protected Neo4jTemplate template; - - public NodeRelationshipFieldAccessorFactory(Neo4jTemplate template) { - this.template = template; - } - -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/NodeToNodesRelationshipFieldAccessor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/NodeToNodesRelationshipFieldAccessor.java deleted file mode 100644 index 2bbdc6fc0..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/NodeToNodesRelationshipFieldAccessor.java +++ /dev/null @@ -1,62 +0,0 @@ -/** - * 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.neo4j.fieldaccess; - -import org.neo4j.graphdb.Direction; -import org.neo4j.graphdb.Node; -import org.neo4j.graphdb.Relationship; -import org.neo4j.graphdb.RelationshipType; -import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty; -import org.springframework.data.neo4j.support.Neo4jTemplate; - -import java.util.HashSet; -import java.util.Set; - -/** - * @author Michael Hunger - * @since 12.09.2010 - */ -public abstract class NodeToNodesRelationshipFieldAccessor extends AbstractNodeRelationshipFieldAccessor { - public NodeToNodesRelationshipFieldAccessor(final Class clazz, final Neo4jTemplate template, final Direction direction, final RelationshipType type, Neo4jPersistentProperty property) { - super(clazz, template, direction, type,property); - } - - @Override - protected Relationship obtainSingleRelationship(final Node start, final Node end) { - final Iterable existingRelationships = start.getRelationships(type, direction); - for (final Relationship existingRelationship : existingRelationships) { - if (existingRelationship!=null && existingRelationship.getOtherNode(start).equals(end)) return existingRelationship; - } - return start.createRelationshipTo(end, type); - } - - @Override - protected Iterable getStatesFromEntity(final Object entity) { - final Node entityNode = getState(entity); - final Set result = new HashSet(); - for (final Relationship rel : entityNode.getRelationships(type, direction)) { - result.add(rel.getOtherNode(entityNode)); - } - return result; - } - - @Override - protected Node getState(final Object entity) { - return template.getPersistentState(entity); - } - -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/OneToNRelationshipEntityFieldAccessorFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/OneToNRelationshipEntityFieldAccessorFactory.java deleted file mode 100644 index 081f3b019..000000000 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/OneToNRelationshipEntityFieldAccessorFactory.java +++ /dev/null @@ -1,146 +0,0 @@ -/** - * 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.neo4j.fieldaccess; - -import org.neo4j.graphdb.Direction; -import org.neo4j.graphdb.Node; -import org.neo4j.graphdb.Relationship; -import org.neo4j.graphdb.RelationshipType; -import org.neo4j.helpers.collection.IteratorUtil; -import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.data.neo4j.mapping.*; -import org.springframework.data.neo4j.support.Neo4jTemplate; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import static org.springframework.data.neo4j.support.DoReturn.doReturn; - -public class OneToNRelationshipEntityFieldAccessorFactory implements FieldAccessorFactory { - - private Neo4jTemplate template; - - public OneToNRelationshipEntityFieldAccessorFactory( - Neo4jTemplate template) { - super(); - this.template = template; - } - - @Override - public boolean accept(final Neo4jPersistentProperty property) { - return property.isRelationship() && !property.getRelationshipInfo().targetsNodes() && property.getRelationshipInfo().isMultiple(); - } - - @Override - public FieldAccessor forField(final Neo4jPersistentProperty property) { - final RelationshipInfo relationshipInfo = property.getRelationshipInfo(); - return new OneToNRelationshipEntityFieldAccessor(relationshipInfo.getRelationshipType(), relationshipInfo.getDirection(), (Class) relationshipInfo.getTargetType().getType(), template,property); - } - public static class OneToNRelationshipEntityFieldAccessor extends AbstractNodeRelationshipFieldAccessor { - - private final boolean isEditableSet; - - public OneToNRelationshipEntityFieldAccessor(final RelationshipType type, final Direction direction, final Class elementClass, final Neo4jTemplate template, Neo4jPersistentProperty property) { - super(elementClass, template, direction, type, property); - isEditableSet = Set.class.isAssignableFrom(this.property.getType()); - } - - @Override - public Object setValue(final Object entity, final Object newVal, MappingPolicy mappingPolicy) { - if (!isEditableSet) throw new InvalidDataAccessApiUsageException("Cannot set read-only relationship entity field."); - final Node startNode = checkUnderlyingState(entity); - if (newVal == null) { - return null; - } - final Map targetNodes = createSetOfTargetNodes(newVal, startNode); - removeMissingRelationships(startNode, targetNodes.keySet()); - //createAddedRelationships(startNode, targetNodes.keySet()); - persistEntities(targetNodes); - return createManagedSet(entity, (Set) newVal, updateMappingPolicy(mappingPolicy)); - } - - private void persistEntities(Map targetNodes) { - for (Object entry : targetNodes.values()) { - template.save(entry); - } - } - - protected Map createSetOfTargetNodes(Object newVal, Node startNode) { - if (!(newVal instanceof Set)) { - throw new IllegalArgumentException("New value must be a Set, was: " + newVal.getClass()); - } - Map targetNodes=new HashMap(); - for (Object entry : (Set) newVal) { - if (!relatedType.isInstance(entry)) { - throw new IllegalArgumentException("New value elements must be "+relatedType); - } - Neo4jPersistentEntity relationshipPEntity = property.getRelationshipInfo().getTargetEntity(); - final RelationshipProperties relationshipProperties = relationshipPEntity.getRelationshipProperties(); - final Neo4jPersistentProperty endNodeProperty = relationshipProperties.getEndNodeProperty(); - final Object endNodeEntity = endNodeProperty.getValue(entry, endNodeProperty.getMappingPolicy()); - final Node endNode = getState(endNodeEntity); - if (!endNode.equals(startNode)) { - targetNodes.put(endNode, entry); - } else { - final Neo4jPersistentProperty startNodeProperty = relationshipProperties.getStartNodeProperty(); - final Node otherNode = getState(startNodeProperty.getValue(entry, startNodeProperty.getMappingPolicy())); - targetNodes.put(otherNode, entry); - } - } - return targetNodes; - } - - @Override - public boolean isWriteable(Object entity) { - return isEditableSet; - } - - @Override - public Object getValue(final Object entity, MappingPolicy mappingPolicy) { - checkUnderlyingState(entity); - final GraphBackedEntityIterableWrapper result = iterableFrom(entity); - if (isEditableSet) { - @SuppressWarnings("unchecked") final ManagedFieldAccessorSet managedSet = createManagedSet(entity, IteratorUtil.addToCollection(result, new HashSet()), updateMappingPolicy(mappingPolicy)); - return doReturn(managedSet); - } - return doReturn(result); - } - - private GraphBackedEntityIterableWrapper iterableFrom(final Object entity) { - return GraphBackedEntityIterableWrapper.create(getStatesFromEntity(entity), relatedType, template); - } - - @Override - protected Iterable getStatesFromEntity(final Object entity) { - final Node node = getState(entity); - return node.getRelationships(type, direction); - } - - @Override - protected Relationship obtainSingleRelationship(final Node start, final Node end) { - return null; - } - - @Override - protected Node getState(final Object entity) { - return template.getPersistentState(entity); - } - - } -} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/ReadOnlyOneToNRelationshipFieldAccessorFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/ReadOnlyRelatedToCollectionFieldAccessorFactory.java similarity index 61% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/ReadOnlyOneToNRelationshipFieldAccessorFactory.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/ReadOnlyRelatedToCollectionFieldAccessorFactory.java index cfe371f78..8c9edc6ac 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/ReadOnlyOneToNRelationshipFieldAccessorFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/ReadOnlyRelatedToCollectionFieldAccessorFactory.java @@ -25,28 +25,30 @@ import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty; import org.springframework.data.neo4j.mapping.RelationshipInfo; import org.springframework.data.neo4j.support.Neo4jTemplate; -public class ReadOnlyOneToNRelationshipFieldAccessorFactory extends NodeRelationshipFieldAccessorFactory { +public class ReadOnlyRelatedToCollectionFieldAccessorFactory implements FieldAccessorFactory { - public ReadOnlyOneToNRelationshipFieldAccessorFactory(Neo4jTemplate template) { - super(template); - } + protected Neo4jTemplate template; + + public ReadOnlyRelatedToCollectionFieldAccessorFactory(Neo4jTemplate template) { + this.template = template; + } @Override - public boolean accept(final Neo4jPersistentProperty f) { - if (!f.isRelationship()) return false; - final RelationshipInfo info = f.getRelationshipInfo(); - return info.isMultiple() && info.targetsNodes() && info.isReadonly(); + public boolean accept(final Neo4jPersistentProperty property) { + if (!property.isRelationship()) return false; + final RelationshipInfo info = property.getRelationshipInfo(); + return info.isCollection() && info.isRelatedTo() && info.isReadonly(); } @Override public FieldAccessor forField(final Neo4jPersistentProperty property) { final RelationshipInfo relationshipInfo = property.getRelationshipInfo(); - return new ReadOnlyOneToNRelationshipFieldAccessor(relationshipInfo.getRelationshipType(), relationshipInfo.getDirection(), (Class) property.getRelationshipInfo().getTargetType().getType(), template,property); + return new ReadOnlyRelatedToCollectionFieldAccessor(relationshipInfo.getRelationshipType(), relationshipInfo.getDirection(), (Class) property.getRelationshipInfo().getTargetType().getType(), template,property); } - public static class ReadOnlyOneToNRelationshipFieldAccessor extends OneToNRelationshipFieldAccessorFactory.OneToNRelationshipFieldAccessor { + public static class ReadOnlyRelatedToCollectionFieldAccessor extends RelatedToCollectionFieldAccessorFactory.RelatedToCollectionFieldAccessor { - public ReadOnlyOneToNRelationshipFieldAccessor(final RelationshipType type, final Direction direction, final Class elementClass, final Neo4jTemplate template, Neo4jPersistentProperty field) { + public ReadOnlyRelatedToCollectionFieldAccessor(final RelationshipType type, final Direction direction, final Class elementClass, final Neo4jTemplate template, Neo4jPersistentProperty field) { super(type,direction,elementClass, template, field); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/OneToNRelationshipFieldAccessorFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelatedToCollectionFieldAccessorFactory.java similarity index 64% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/OneToNRelationshipFieldAccessorFactory.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelatedToCollectionFieldAccessorFactory.java index 5a6b8ad6d..10d47791f 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/OneToNRelationshipFieldAccessorFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelatedToCollectionFieldAccessorFactory.java @@ -24,61 +24,63 @@ import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty; import org.springframework.data.neo4j.mapping.RelationshipInfo; import org.springframework.data.neo4j.support.Neo4jTemplate; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; import static org.springframework.data.neo4j.support.DoReturn.doReturn; -public class OneToNRelationshipFieldAccessorFactory extends NodeRelationshipFieldAccessorFactory { - - public OneToNRelationshipFieldAccessorFactory(Neo4jTemplate template) { - super(template); - } +public class RelatedToCollectionFieldAccessorFactory implements FieldAccessorFactory { + + protected Neo4jTemplate template; + + public RelatedToCollectionFieldAccessorFactory(Neo4jTemplate template) { + this.template = template; + } @Override public boolean accept(final Neo4jPersistentProperty property) { if (!property.isRelationship()) return false; final RelationshipInfo info = property.getRelationshipInfo(); - return info.isMultiple() && info.targetsNodes() && !info.isReadonly(); + return info.isCollection() && info.isRelatedTo() && !info.isReadonly(); } @Override public FieldAccessor forField(final Neo4jPersistentProperty property) { final RelationshipInfo relationshipInfo = property.getRelationshipInfo(); final Class targetType = relationshipInfo.getTargetType().getType(); - return new OneToNRelationshipFieldAccessor(relationshipInfo.getRelationshipType(), relationshipInfo.getDirection(), targetType, template,property); + return new RelatedToCollectionFieldAccessor(relationshipInfo.getRelationshipType(), relationshipInfo.getDirection(), targetType, template,property); } - public static class OneToNRelationshipFieldAccessor extends NodeToNodesRelationshipFieldAccessor { + public static class RelatedToCollectionFieldAccessor extends RelatedToFieldAccessor { - public OneToNRelationshipFieldAccessor(final RelationshipType type, final Direction direction, final Class elementClass, final Neo4jTemplate template, Neo4jPersistentProperty property) { + public RelatedToCollectionFieldAccessor(final RelationshipType type, final Direction direction, final Class elementClass, final Neo4jTemplate template, Neo4jPersistentProperty property) { super(elementClass, template, direction, type,property); } public Object setValue(final Object entity, final Object newVal, MappingPolicy mappingPolicy) { - final Node node = checkUnderlyingState(entity); - if (newVal == null) { -/* null should not remove existing relationships but leave them alone - removeMissingRelationships(node, Collections.emptySet()); -*/ - return null; - } + final Node node = checkAndGetNode(entity); +// null should not remove existing relationships but leave them alone + if (newVal == null) return null; final Set targetNodes = createSetOfTargetNodes(newVal); removeMissingRelationships(node, targetNodes); createAddedRelationships(node, targetNodes); - return createManagedSet(entity, (Set) newVal, updateMappingPolicy(mappingPolicy)); + return createManagedSet(entity, (Set) newVal, property.obtainMappingPolicy(mappingPolicy)); } @Override public Object getValue(final Object entity, MappingPolicy mappingPolicy) { - checkUnderlyingState(entity); - final MappingPolicy currentPolicy = updateMappingPolicy(mappingPolicy); + checkAndGetNode(entity); + final MappingPolicy currentPolicy = property.obtainMappingPolicy(mappingPolicy); final Set result = createEntitySetFromRelationshipEndNodes(entity, currentPolicy); return doReturn(createManagedSet(entity, result, currentPolicy)); } @Override public Object getDefaultValue() { + // todo delegate to property + if (List.class.isAssignableFrom(property.getType())) return new ArrayList(); return new HashSet(); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelatedToFieldAccessor.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelatedToFieldAccessor.java new file mode 100644 index 000000000..ebdca873d --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelatedToFieldAccessor.java @@ -0,0 +1,83 @@ +/** + * 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.neo4j.fieldaccess; + +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.RelationshipType; +import org.springframework.data.neo4j.mapping.MappingPolicy; +import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty; +import org.springframework.data.neo4j.support.Neo4jTemplate; + +import java.util.Set; + +/** + * @author Michael Hunger + * @since 11.09.2010 + */ +public abstract class RelatedToFieldAccessor implements FieldAccessor { + protected final RelationshipType type; + protected final Neo4jPersistentProperty property; + protected final Direction direction; + protected final Class relatedType; + protected final Neo4jTemplate template; + protected RelationshipHelper relationshipHelper; + + public RelatedToFieldAccessor(Class relatedType, Neo4jTemplate template, Direction direction, RelationshipType type, Neo4jPersistentProperty property) { + this.relationshipHelper = new RelationshipHelper(template, direction, type); + this.relatedType = relatedType; + this.template = template; + this.direction = direction; + this.type = type; + this.property = property; + } + + @Override + public boolean isWriteable(Object entity) { + return true; + } + + protected ManagedFieldAccessorSet createManagedSet(Object entity, Set result, MappingPolicy mappingPolicy) { + return ManagedFieldAccessorSet.create(entity, result, mappingPolicy, property, template, this); + } + + public Object getDefaultValue() { + return null; + } + + // delegating methods + + protected Node checkAndGetNode(Object entity) { + return relationshipHelper.checkAndGetNode(entity); + } + + protected void removeMissingRelationships(Node node, Set targetNodes) { + relationshipHelper.removeMissingRelationshipsInStoreAndKeepOnlyNewRelationShipsInSet(node, targetNodes); + } + + protected void createAddedRelationships(Node node, Set targetNodes) { + relationshipHelper.createAddedRelationships(node, targetNodes); + } + + protected Set createSetOfTargetNodes(Object newVal) { + return relationshipHelper.createSetOfTargetNodes(newVal, relatedType); + } + + protected Set createEntitySetFromRelationshipEndNodes(Object entity, MappingPolicy mappingPolicy) { + return relationshipHelper.createEntitySetFromRelationshipEndNodes(entity, mappingPolicy, relatedType); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/SingleRelationshipFieldAccessorFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelatedToSingleFieldAccessorFactory.java similarity index 69% rename from spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/SingleRelationshipFieldAccessorFactory.java rename to spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelatedToSingleFieldAccessorFactory.java index 6c52fc1a0..844847974 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/SingleRelationshipFieldAccessorFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelatedToSingleFieldAccessorFactory.java @@ -30,31 +30,34 @@ import java.util.Set; import static org.springframework.data.neo4j.support.DoReturn.doReturn; -public class SingleRelationshipFieldAccessorFactory extends NodeRelationshipFieldAccessorFactory { +public class RelatedToSingleFieldAccessorFactory implements FieldAccessorFactory { - public SingleRelationshipFieldAccessorFactory(Neo4jTemplate template) { - super(template); + protected Neo4jTemplate template; + + public RelatedToSingleFieldAccessorFactory(Neo4jTemplate template) { + this.template = template; } @Override public boolean accept(final Neo4jPersistentProperty property) { - return property.isRelationship() && property.getRelationshipInfo().targetsNodes() && !property.getRelationshipInfo().isMultiple(); + if (!property.isRelationship()) return false; + return property.getRelationshipInfo().isRelatedTo() && property.getRelationshipInfo().isSingle(); } @Override public FieldAccessor forField(final Neo4jPersistentProperty property) { final RelationshipInfo relationshipInfo = property.getRelationshipInfo(); - return new SingleRelationshipFieldAccessor(relationshipInfo.getRelationshipType(), relationshipInfo.getDirection(), (Class) relationshipInfo.getTargetType().getType(), template,property); + return new RelatedToSingleFieldAccessor(relationshipInfo.getRelationshipType(), relationshipInfo.getDirection(), (Class) relationshipInfo.getTargetType().getType(), template,property); } - public static class SingleRelationshipFieldAccessor extends NodeToNodesRelationshipFieldAccessor { - public SingleRelationshipFieldAccessor(final RelationshipType type, final Direction direction, final Class clazz, final Neo4jTemplate template, Neo4jPersistentProperty property) { + public static class RelatedToSingleFieldAccessor extends RelatedToFieldAccessor { + public RelatedToSingleFieldAccessor(final RelationshipType type, final Direction direction, final Class clazz, final Neo4jTemplate template, Neo4jPersistentProperty property) { super(clazz, template, direction, type, property); } @Override public Object setValue(final Object entity, final Object newVal, MappingPolicy mappingPolicy) { - final Node node= checkUnderlyingState(entity); + final Node node= checkAndGetNode(entity); if (newVal == null) { removeMissingRelationships(node, Collections.emptySet()); return null; @@ -67,8 +70,8 @@ public class SingleRelationshipFieldAccessorFactory extends NodeRelationshipFiel @Override public Object getValue(final Object entity, MappingPolicy mappingPolicy) { - checkUnderlyingState(entity); - final Set result = createEntitySetFromRelationshipEndNodes(entity, updateMappingPolicy(mappingPolicy)); + checkAndGetNode(entity); + final Set result = createEntitySetFromRelationshipEndNodes(entity, property.obtainMappingPolicy(mappingPolicy)); final Object singleEntity = result.isEmpty() ? null : result.iterator().next(); return doReturn(singleEntity); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelatedToViaCollectionFieldAccessorFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelatedToViaCollectionFieldAccessorFactory.java new file mode 100644 index 000000000..c45c5895f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelatedToViaCollectionFieldAccessorFactory.java @@ -0,0 +1,135 @@ +/** + * 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.neo4j.fieldaccess; + +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.RelationshipType; +import org.neo4j.helpers.collection.IteratorUtil; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.neo4j.mapping.MappingPolicy; +import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty; +import org.springframework.data.neo4j.mapping.RelationshipInfo; +import org.springframework.data.neo4j.support.Neo4jTemplate; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.springframework.data.neo4j.support.DoReturn.doReturn; + +public class RelatedToViaCollectionFieldAccessorFactory implements FieldAccessorFactory { + + private Neo4jTemplate template; + + public RelatedToViaCollectionFieldAccessorFactory(Neo4jTemplate template) { + this.template = template; + } + + @Override + public boolean accept(final Neo4jPersistentProperty property) { + if (!property.isRelationship()) return false; + return property.getRelationshipInfo().isRelatedToVia() && property.getRelationshipInfo().isCollection(); + } + + @Override + public FieldAccessor forField(final Neo4jPersistentProperty property) { + final RelationshipInfo relationshipInfo = property.getRelationshipInfo(); + return new RelatedToViaCollectionFieldAccessor(relationshipInfo.getRelationshipType(), relationshipInfo.getDirection(), (Class) relationshipInfo.getTargetType().getType(), template,property); + } + + public static class RelatedToViaCollectionFieldAccessor implements FieldAccessor { + + private final boolean isMutableCollection; + private final Class relatedType; + private final Neo4jTemplate template; + private final Neo4jPersistentProperty property; + private final RelationshipHelper relationshipHelper; + private final RelationshipEntities relationshipEntities; + + public RelatedToViaCollectionFieldAccessor(final RelationshipType type, final Direction direction, final Class relatedType, final Neo4jTemplate template, Neo4jPersistentProperty property) { + relationshipHelper = new RelationshipHelper(template, direction, type); + this.relatedType = relatedType; + this.template = template; + this.property = property; + isMutableCollection = Collection.class.isAssignableFrom(property.getType()); + relationshipEntities = new RelationshipEntities(relationshipHelper, property); + } + + @Override + public Object getDefaultValue() { + // todo delegate to property + if (List.class.isAssignableFrom(property.getType())) return new ArrayList(); + return new HashSet(); + } + + @Override + public Object setValue(final Object entity, final Object newVal, MappingPolicy mappingPolicy) { + if (!isMutableCollection) throw new InvalidDataAccessApiUsageException("Cannot set read-only relationship entity field."); + final Node startNode = relationshipHelper.checkAndGetNode(entity); + // null collections values are ignored, not deleting relationships + if (newVal == null) return null; + + final Map endNodeToEntityMapping = loadEndNodeToRelationshipEntityMapping(newVal, startNode); + relationshipHelper.removeMissingRelationshipsInStoreAndKeepOnlyNewRelationShipsInSet(startNode, endNodeToEntityMapping.keySet()); + persistEntities(endNodeToEntityMapping.values()); + return createManagedSet(entity, (Set) newVal, property.obtainMappingPolicy(mappingPolicy)); + } + + private void persistEntities(final Collection relationshipEntities) { + for (Object entity : relationshipEntities) { + template.save(entity); + } + } + + protected Map loadEndNodeToRelationshipEntityMapping(Object newVal, Node startNode) { + if (!(newVal instanceof Set)) { + throw new IllegalArgumentException("New value must be at least an Iterable, was: " + newVal.getClass()); + } + return relationshipEntities.loadEndNodeToRelationshipEntityMapping(startNode, (Iterable) newVal, relatedType); + } + + + @Override + public boolean isWriteable(Object entity) { + return isMutableCollection; + } + + @Override + public Object getValue(final Object entity, MappingPolicy mappingPolicy) { + final Node node = relationshipHelper.checkAndGetNode(entity); + final GraphBackedEntityIterableWrapper result = loadRelationshipEntities(node); + if (isMutableCollection) { + @SuppressWarnings("unchecked") final ManagedFieldAccessorSet managedSet = createManagedSet(entity, IteratorUtil.addToCollection(result, new HashSet()), property.obtainMappingPolicy(mappingPolicy)); + return doReturn(managedSet); + } + return doReturn(result); + } + + protected ManagedFieldAccessorSet createManagedSet(Object entity, Set result, MappingPolicy mappingPolicy) { + return ManagedFieldAccessorSet.create(entity, result, mappingPolicy, property, template, this); + } + + private GraphBackedEntityIterableWrapper loadRelationshipEntities(final Node node) { + return GraphBackedEntityIterableWrapper.create(relationshipHelper.getRelationships(node), relatedType, template); + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelatedToViaSingleFieldAccessorFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelatedToViaSingleFieldAccessorFactory.java new file mode 100644 index 000000000..d11e7a089 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelatedToViaSingleFieldAccessorFactory.java @@ -0,0 +1,108 @@ +/** + * 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.neo4j.fieldaccess; + +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.RelationshipType; +import org.springframework.data.neo4j.mapping.MappingPolicy; +import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty; +import org.springframework.data.neo4j.mapping.RelationshipInfo; +import org.springframework.data.neo4j.support.Neo4jTemplate; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import static org.springframework.data.neo4j.support.DoReturn.doReturn; + +public class RelatedToViaSingleFieldAccessorFactory implements FieldAccessorFactory { + + private Neo4jTemplate template; + + public RelatedToViaSingleFieldAccessorFactory(Neo4jTemplate template) { + this.template = template; + } + + @Override + public boolean accept(final Neo4jPersistentProperty property) { + if (!property.isRelationship()) return false; + return property.getRelationshipInfo().isRelatedToVia() && property.getRelationshipInfo().isSingle(); + } + + @Override + public FieldAccessor forField(final Neo4jPersistentProperty property) { + final RelationshipInfo relationshipInfo = property.getRelationshipInfo(); + return new RelatedToViaSingleFieldAccessor(relationshipInfo.getRelationshipType(), relationshipInfo.getDirection(), (Class) relationshipInfo.getTargetType().getType(), template,property); + } + + public static class RelatedToViaSingleFieldAccessor implements FieldAccessor { + + private final Class relatedType; + private final Neo4jTemplate template; + private final Neo4jPersistentProperty property; + private final RelationshipHelper relationshipHelper; + private final RelationshipEntities relationshipEntities; + + public RelatedToViaSingleFieldAccessor(final RelationshipType type, final Direction direction, final Class relatedType, final Neo4jTemplate template, Neo4jPersistentProperty property) { + relationshipHelper = new RelationshipHelper(template, direction, type); + relationshipEntities = new RelationshipEntities(relationshipHelper, property); + this.relatedType = relatedType; + this.template = template; + this.property = property; + + } + + @Override + public Object getDefaultValue() { + return null; + } + + @Override + public Object setValue(final Object entity, final Object newVal, MappingPolicy mappingPolicy) { + final Node startNode = relationshipHelper.checkAndGetNode(entity); + final Map endNodeToEntityMapping = relationshipEntities.loadEndNodeToRelationshipEntityMapping(startNode, toSet(newVal), relatedType); + relationshipHelper.removeMissingRelationshipsInStoreAndKeepOnlyNewRelationShipsInSet(startNode, endNodeToEntityMapping.keySet()); + persistEntities(endNodeToEntityMapping.values()); + return newVal; + } + + private Iterable toSet(Object newVal) { + if (newVal==null) return Collections.emptySet(); + return Collections.singleton(newVal); + } + + private void persistEntities(final Collection relationshipEntities) { + for (Object entity : relationshipEntities) { + template.save(entity); + } + } + + @Override + public boolean isWriteable(Object entity) { + return true; + } + + @Override + public Object getValue(final Object entity, MappingPolicy mappingPolicy) { + final Node node = relationshipHelper.checkAndGetNode(entity); + Relationship rel = relationshipHelper.getSingleRelationship(node); + return doReturn(rel==null ? null : template.load(rel,relatedType)); + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelationshipEntities.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelationshipEntities.java new file mode 100644 index 000000000..934c67648 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelationshipEntities.java @@ -0,0 +1,68 @@ +/** + * 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.neo4j.fieldaccess; + +import org.neo4j.graphdb.Node; +import org.springframework.data.neo4j.mapping.MappingPolicy; +import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty; +import org.springframework.data.neo4j.mapping.RelationshipProperties; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author mh + * @since 28.02.12 + */ +class RelationshipEntities { + + private final RelationshipHelper relationshipHelper; + private final Neo4jPersistentProperty property; + private final RelationshipProperties relationshipProperties; + private final Neo4jPersistentProperty endNodeProperty; + private final Neo4jPersistentProperty startNodeProperty; + private final MappingPolicy endNodeMappingPolicy; + private final MappingPolicy startNodeMappingPolicy; + + public RelationshipEntities(RelationshipHelper relationshipHelper, Neo4jPersistentProperty property) { + this.relationshipHelper = relationshipHelper; + this.property = property; + relationshipProperties = property.getRelationshipInfo().getTargetEntity().getRelationshipProperties(); + endNodeProperty = relationshipProperties.getEndNodeProperty(); + startNodeProperty = relationshipProperties.getStartNodeProperty(); + endNodeMappingPolicy = endNodeProperty.getMappingPolicy(); + startNodeMappingPolicy = startNodeProperty.getMappingPolicy(); + } + + public Node getOtherNode(Node startNode, Object relationshipEntity) { + final Node endNode = relationshipHelper.getNode(endNodeProperty.getValue(relationshipEntity, endNodeMappingPolicy)); + if (startNode.equals(endNode)) { + return relationshipHelper.getNode(startNodeProperty.getValue(relationshipEntity, startNodeMappingPolicy)); + } else { + return endNode; + } + } + + public Map loadEndNodeToRelationshipEntityMapping(Node startNode, Iterable values, Class relatedType) { + Map endNodeToEntityMapping = new HashMap(); + for (Object entry : values) { + if (!relatedType.isInstance(entry)) + throw new IllegalArgumentException("Elements of " + property + " collection must be of " + relatedType); + endNodeToEntityMapping.put(getOtherNode(startNode, entry), entry); + } + return endNodeToEntityMapping; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelationshipHelper.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelationshipHelper.java new file mode 100644 index 000000000..3702b8f0a --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelationshipHelper.java @@ -0,0 +1,151 @@ +/** + * 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.neo4j.fieldaccess; + +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.Relationship; +import org.neo4j.graphdb.RelationshipType; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.neo4j.mapping.MappingPolicy; +import org.springframework.data.neo4j.support.Neo4jTemplate; +import org.springframework.util.Assert; + +import java.util.HashSet; +import java.util.Set; + +/** + * @author mh + * @since 28.02.12 + */ +public class RelationshipHelper { + + private final Neo4jTemplate template; + private final Direction direction; + private final RelationshipType type; + + public RelationshipHelper(Neo4jTemplate template, Direction direction, RelationshipType type) { + this.template = template; + this.direction = direction; + this.type = type; + } + + private Iterable getOtherNodes(Node node) { + final Set result = new HashSet(); + for (final Relationship rel : node.getRelationships(type, direction)) { + result.add(rel.getOtherNode(node)); + } + return result; + } + + protected Relationship obtainSingleRelationship(final Node start, final Node end) { + final Iterable existingRelationships = start.getRelationships(type, direction); + for (final Relationship existingRelationship : existingRelationships) { + if (existingRelationship != null && existingRelationship.getOtherNode(start).equals(end)) + return existingRelationship; + } + return start.createRelationshipTo(end, type); + } + + protected Node checkAndGetNode(Object entity) { + if (entity == null) throw new IllegalStateException("Entity is null"); + Node node = getNode(entity); + if (node != null) return node; + throw new IllegalStateException("Entity must have a backing Node"); + } + + protected void removeMissingRelationshipsInStoreAndKeepOnlyNewRelationShipsInSet(Node node, Set targetNodes) { + for (Relationship relationship : node.getRelationships(type, direction)) { + if (!targetNodes.remove(relationship.getOtherNode(node))) + relationship.delete(); + } + } + + protected void createAddedRelationships(Node node, Set targetNodes) { + for (Node targetNode : targetNodes) { + createSingleRelationship(node, targetNode); + } + } + + // adding cascade + @SuppressWarnings("unchecked") + protected Set createSetOfTargetNodes(Object newVal, final Class relatedType) { + if (!(newVal instanceof Set)) { + throw new IllegalArgumentException("New value must be a Set, was: " + newVal.getClass()); + } + Set nodes = new HashSet(); + for (Object value : (Set) newVal) { + if (!relatedType.isInstance(value)) { + throw new IllegalArgumentException("New value elements must be " + relatedType); + } + nodes.add(getOrCreateState(value)); + } + return nodes; + } + + protected Node getOrCreateState(Object value) { + final Node Node = getNode(value); + if (Node != null) return Node; + final Object saved = template.save(value); + final Node newState = getNode(saved); + Assert.notNull(newState); + return newState; + } + + + protected Set createEntitySetFromRelationshipEndNodes(Object entity, final MappingPolicy mappingPolicy, final Class relatedType) { + final Iterable nodes = getStatesFromEntity(entity); + final Set result = new HashSet(); + for (final Node otherNode : nodes) { + Object target = template.createEntityFromState(otherNode, relatedType, mappingPolicy); + result.add(target); + } + return result; + } + + @SuppressWarnings("unchecked") + protected Relationship createSingleRelationship(Node start, Node end) { + if (end == null) return null; + switch (direction) { + case OUTGOING: + case BOTH: { // TODO both should actually check in both directions, perhaps have the obtain method get the direction instead and figure out what to do itself + return obtainSingleRelationship(start, end); + } + case INCOMING: + return obtainSingleRelationship(end, start); + default: + throw new InvalidDataAccessApiUsageException("invalid direction " + direction); + } + } + + protected Iterable getStatesFromEntity(final Object entity) { + final Node node = getNode(entity); + return getOtherNodes(node); + } + + + protected Node getNode(final Object entity) { + return template.getPersistentState(entity); + } + + public Iterable getRelationships(Node node) { + return node.getRelationships(type, direction); + } + + public Relationship getSingleRelationship(Node node) { + return node.getSingleRelationship(type,direction); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/mapping/Neo4jPersistentProperty.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/mapping/Neo4jPersistentProperty.java index f43406b66..a7e48d441 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/mapping/Neo4jPersistentProperty.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/mapping/Neo4jPersistentProperty.java @@ -92,4 +92,6 @@ public interface Neo4jPersistentProperty extends PersistentProperty getPropertyType(); boolean isUnique(); + + MappingPolicy obtainMappingPolicy(MappingPolicy currentMappingPolicy); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/mapping/RelationshipInfo.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/mapping/RelationshipInfo.java index 9baf0c189..6e3568b18 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/mapping/RelationshipInfo.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/mapping/RelationshipInfo.java @@ -30,11 +30,11 @@ import java.lang.reflect.Field; public class RelationshipInfo { - private boolean isMultiple; + private boolean isCollection; private final Direction direction; private final String type; private final TypeInformation targetType; - private final boolean targetsNodes; + private final boolean relatedTo; private boolean readonly; private Neo4jPersistentEntity targetEntity; @@ -49,18 +49,21 @@ public class RelationshipInfo { return DynamicRelationshipType.withName(type); } - public boolean isMultiple() { - return isMultiple; + public boolean isCollection() { + return isCollection; + } + public boolean isSingle() { + return !isCollection; } public RelationshipInfo(String type, Direction direction, TypeInformation typeInformation, TypeInformation concreteActualType, Neo4jMappingContext ctx) { this.type = type; this.direction = direction; - isMultiple = typeInformation.isCollectionLike(); + isCollection = typeInformation.isCollectionLike(); targetType = concreteActualType!=null ? concreteActualType : typeInformation.getActualType(); this.targetEntity = ctx.getPersistentEntity(targetType); - targetsNodes = targetEntity.isNodeEntity(); - this.readonly = isMultiple() && typeInformation.getType().equals(Iterable.class); + relatedTo = targetEntity.isNodeEntity(); + this.readonly = isCollection() && typeInformation.getType().equals(Iterable.class); } public static RelationshipInfo fromField(Field field, TypeInformation typeInformation, Neo4jMappingContext ctx) { @@ -104,8 +107,11 @@ public class RelationshipInfo { return targetType; } - public boolean targetsNodes() { - return targetsNodes; + public boolean isRelatedTo() { + return relatedTo; + } + public boolean isRelatedToVia() { + return !isRelatedTo(); } public boolean isReadonly() { diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/Neo4jTemplate.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/Neo4jTemplate.java index b52521df1..98e4dc031 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/Neo4jTemplate.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/Neo4jTemplate.java @@ -34,6 +34,7 @@ import org.springframework.data.neo4j.conversion.ResultConverter; import org.springframework.data.neo4j.core.GraphDatabase; import org.springframework.data.neo4j.core.TypeRepresentationStrategy; import org.springframework.data.neo4j.core.UncategorizedGraphStoreException; +import org.springframework.data.neo4j.fieldaccess.GraphBackedEntityIterableWrapper; import org.springframework.data.neo4j.mapping.EntityPersister; import org.springframework.data.neo4j.mapping.IndexInfo; import org.springframework.data.neo4j.mapping.MappingPolicy; @@ -253,8 +254,8 @@ public class Neo4jTemplate implements Neo4jOperations, EntityPersister { * properties are used to initialize the node. */ @Override - public Node getOrCreateNode(String index, String key, Object value, final Map properties) { - return infrastructure.getGraphDatabase().getOrCreateNode(index,key,value,properties); + public Node getOrCreateNode(String index, String key, Object value, final Map properties) { + return infrastructure.getGraphDatabase().getOrCreateNode(index, key, value, properties); } @Override @@ -341,6 +342,15 @@ public class Neo4jTemplate implements Neo4jOperations, EntityPersister { return infrastructure.getEntityPersister().createEntityFromState(relationship, relationshipEntityClass, persistentEntity.getMappingPolicy()); } + @Override + public Iterable getRelationshipsBetween(Object start, Object end, Class relationshipEntityClass, String relationshipType) { + notNull(start,"start",end,"end",relationshipEntityClass,"relationshipEntityClass",relationshipType,"relationshipType"); + final Iterable relationships = infrastructure.getEntityStateHandler().getRelationshipsBetween(start, end, relationshipType); + if (relationships == null) return null; + if (Relationship.class.isAssignableFrom(relationshipEntityClass)) return (Iterable)relationships; + return GraphBackedEntityIterableWrapper.create(relationships, relationshipEntityClass, this); + } + @Override public Relationship getRelationshipBetween(Object start, Object end, String relationshipType) { notNull(start,"start",end,"end",relationshipType,"relationshipType"); diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/mapping/EntityStateHandler.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/mapping/EntityStateHandler.java index ce09369a2..3a29e29da 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/mapping/EntityStateHandler.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/mapping/EntityStateHandler.java @@ -31,7 +31,9 @@ import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty; import org.springframework.data.neo4j.mapping.RelationshipProperties; import org.springframework.data.neo4j.mapping.RelationshipResult; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; /** * @author mh @@ -245,4 +247,17 @@ public class EntityStateHandler { } + public Iterable getRelationshipsBetween(Object source, Object target, String type) { + if (source == null) throw new IllegalArgumentException("Source entity is null"); + if (target == null) throw new IllegalArgumentException("Target entity is null"); + if (type == null) throw new IllegalArgumentException("Relationshiptype is null"); + Node node = getPersistentState(source); + Node targetNode = getPersistentState(target); + if (node == null || targetNode == null) return null; + List result=new ArrayList(); + for (Relationship relationship : node.getRelationships(DynamicRelationshipType.withName(type),Direction.OUTGOING)) { + if (relationship.getEndNode().equals(targetNode)) result.add(relationship); + } + return result; + } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/mapping/Neo4JPersistentPropertyImpl.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/mapping/Neo4JPersistentPropertyImpl.java index a4fbc94ff..7e64fdc33 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/mapping/Neo4JPersistentPropertyImpl.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/support/mapping/Neo4JPersistentPropertyImpl.java @@ -348,4 +348,9 @@ class Neo4jPersistentPropertyImpl extends AbstractPersistentProperty R getRelationshipBetween(Object start, Object end, Class relationshipEntityClass, String relationshipType); + /** + * Retrieves all relationship entities between two node entities with the given relationship type projected to the provided + * relationship entity class + */ + Iterable getRelationshipsBetween(Object start, Object end, Class relationshipEntityClass, String relationshipType); + /** * Retrieves a single relationship entity between two node entities. */ diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/EntityNeo4jTemplateTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/EntityNeo4jTemplateTest.java index 3f4a76b33..23ff992bf 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/EntityNeo4jTemplateTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/EntityNeo4jTemplateTest.java @@ -50,6 +50,7 @@ import org.springframework.transaction.support.TransactionTemplate; import java.util.*; import static org.junit.Assert.*; +import static org.junit.internal.matchers.IsCollectionContaining.hasItems; import static org.neo4j.graphdb.Direction.OUTGOING; import static org.neo4j.helpers.collection.IteratorUtil.asCollection; import static org.neo4j.helpers.collection.IteratorUtil.first; @@ -295,6 +296,13 @@ public class EntityNeo4jTemplateTest extends EntityTestBase { final Friendship knows = neo4jOperations.getRelationshipBetween(testTeam.michael, testTeam.david, Friendship.class, "knows"); assertEquals(testTeam.friendShip.getId(),knows.getId()); } + @Test @Transactional + public void testGetMultipleRelationshipBetween() throws Exception { + final Friendship friendship = neo4jOperations.getRelationshipBetween(testTeam.michael, testTeam.david, Friendship.class, "knows"); + final Friendship friendship2 = neo4jOperations.createRelationshipBetween(testTeam.michael, testTeam.david, Friendship.class, "knows", true); + final Iterable allFriendships = neo4jOperations.getRelationshipsBetween(testTeam.michael, testTeam.david, Friendship.class, "knows"); + assertThat(allFriendships, hasItems(friendship, friendship2)); + } @Test @Transactional public void testDeleteRelationshipBetween() throws Exception { diff --git a/src/docbkx/reference/programming-model/relationships.xml b/src/docbkx/reference/programming-model/relationships.xml index f08ad165a..3314e8b06 100644 --- a/src/docbkx/reference/programming-model/relationships.xml +++ b/src/docbkx/reference/programming-model/relationships.xml @@ -122,19 +122,19 @@ public class Actor { To access the full data model of graph relationships, POJOs can also be annotated with @RelationshipEntity, making them relationship entities. Just as node entities represent nodes in the graph, relationship entities represent relationships. As described above, - fields annotated with @RelatedTo provide a way to link node entities together + fields annotated with @RelatedTo provide a way to only link node entities via relationships, but it provides no way of accessing the relationships themselves. Relationship entities can be accessed via by @RelatedToVia-annotated () fields or methods like entity.getRelationshipTo() - or template|repository.getRelationshipsBetween(). + or template|repository.getRelationship(s)Between(). - Relationship entities either be instantiated directly and added to - Set's of @RelatedToVia fields or created by the introduced + Relationship entities either be instantiated directly and set or added to + @RelatedToVia-annotated fields or created by the introduced entity.relateTo(), template|repository.createRelationshipBetween() methods - (see ) + (see alos ) Fields in relationship entities are, similarly to node entities, persisted as properties on @@ -174,19 +174,22 @@ public class Role { To provide easy programmatic access to the richer relationship entities of the data model, the annotation @RelatedToVia can be added on fields of type - Iterable<T> or Set<T>, where T is a @RelationshipEntity-annotated + Iterable<T> or Set<T> or T, where T is a @RelationshipEntity-annotated class. These fields provide access to relationship entities. Relationship entity (in simple mapping) roles=new HashSet(); public Role playedIn(Movie movie, String title) { Role role=new Role(this,movie,title); roles.add(role); return role; } + @RelatedToVia(type="FRIEND_OF", direction=Direction.INCOMING) + Friendship bestFriend; } @RelationshipEntity(type = "ACTS_IN") @@ -195,6 +198,13 @@ public class Role { @StartNode private Actor actor; @EndNode private Movie movie; +} +@RelationshipEntity +public class Friendship { + Date since; + + @StartNode private Actor actor; + @EndNode private Person buddy; } ]]>