From 635fe4e17f96a91e8db4d80b4d180e59c778e77a Mon Sep 17 00:00:00 2001 From: Lasse Westh-Nielsen Date: Fri, 31 Aug 2012 11:15:43 +0100 Subject: [PATCH] DATAGRAPH-285 adding application events for save and delete --- .../data/neo4j/lifecycle/AfterSaveEvent.java | 38 ++ .../data/neo4j/lifecycle/BeforeSaveEvent.java | 36 ++ .../data/neo4j/lifecycle/DeleteEvent.java | 32 ++ .../data/neo4j/support/Neo4jTemplate.java | 108 +++--- .../neo4j/lifecycle/AfterSaveEventTests.java | 81 +++++ .../neo4j/lifecycle/BeforeSaveEventTests.java | 79 +++++ .../neo4j/lifecycle/DeleteEventTests.java | 107 ++++++ .../data/neo4j/lifecycle/SaveEventTests.java | 332 ++++++++++++++++++ .../reference/programming-model/template.xml | 31 ++ 9 files changed, 798 insertions(+), 46 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/lifecycle/AfterSaveEvent.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/lifecycle/BeforeSaveEvent.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/lifecycle/DeleteEvent.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/lifecycle/AfterSaveEventTests.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/lifecycle/BeforeSaveEventTests.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/lifecycle/DeleteEventTests.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/lifecycle/SaveEventTests.java diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/lifecycle/AfterSaveEvent.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/lifecycle/AfterSaveEvent.java new file mode 100644 index 0000000000..4e3762708d --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/lifecycle/AfterSaveEvent.java @@ -0,0 +1,38 @@ +/** + * 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.lifecycle; + +import org.springframework.context.ApplicationEvent; + +public class AfterSaveEvent extends ApplicationEvent { + private final T entity; + + /** + * Create a new ApplicationEvent. + * + * @param source the component that published the event (never null) + * @param entity + */ + public AfterSaveEvent(Object source, T entity) { + super(source); + + this.entity = entity; + } + + public T getEntity() { + return entity; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/lifecycle/BeforeSaveEvent.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/lifecycle/BeforeSaveEvent.java new file mode 100644 index 0000000000..82580ba629 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/lifecycle/BeforeSaveEvent.java @@ -0,0 +1,36 @@ +/** + * 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.lifecycle; + +import org.springframework.context.ApplicationEvent; + +public class BeforeSaveEvent extends ApplicationEvent { + private final T entity; + + /** + * @param source the component that published the event (never null) + * @param entity the entity that is about to be saved + */ + public BeforeSaveEvent(Object source, T entity) { + super(source); + + this.entity = entity; + } + + public T getEntity() { + return entity; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/lifecycle/DeleteEvent.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/lifecycle/DeleteEvent.java new file mode 100644 index 0000000000..7b67ca021f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/lifecycle/DeleteEvent.java @@ -0,0 +1,32 @@ +/** + * 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.lifecycle; + +import org.springframework.context.ApplicationEvent; + +public class DeleteEvent extends ApplicationEvent { + private final T entity; + + public DeleteEvent(Object source, T entity) { + super(source); + + this.entity = entity; + } + + public T getEntity() { + return entity; + } +} 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 21fa225543..d9ddb377c4 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 @@ -23,6 +23,9 @@ import org.neo4j.index.lucene.ValueContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.core.convert.ConversionService; import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -36,6 +39,9 @@ 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.lifecycle.AfterSaveEvent; +import org.springframework.data.neo4j.lifecycle.BeforeSaveEvent; +import org.springframework.data.neo4j.lifecycle.DeleteEvent; import org.springframework.data.neo4j.mapping.IndexInfo; import org.springframework.data.neo4j.mapping.MappingPolicy; import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty; @@ -45,11 +51,7 @@ import org.springframework.data.neo4j.repository.RelationshipGraphRepository; import org.springframework.data.neo4j.support.index.IndexProvider; import org.springframework.data.neo4j.support.index.IndexType; -import org.springframework.data.neo4j.support.mapping.EntityStateHandler; -import org.springframework.data.neo4j.support.mapping.Neo4jEntityPersister; -import org.springframework.data.neo4j.support.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.support.mapping.Neo4jPersistentEntityImpl; -import org.springframework.data.neo4j.support.mapping.StoredEntityType; +import org.springframework.data.neo4j.support.mapping.*; import org.springframework.data.neo4j.support.query.QueryEngine; import org.springframework.data.neo4j.template.GraphCallback; import org.springframework.data.neo4j.template.Neo4jOperations; @@ -78,10 +80,11 @@ /* TODO This is a merge of GraphDatabaseContext and the previous Neo4jTemplate, so it still contains inconsistencies, if you spot them, please mark them with a TODO */ -public class Neo4jTemplate implements Neo4jOperations { +public class Neo4jTemplate implements Neo4jOperations, ApplicationContextAware { private static final Logger log = LoggerFactory.getLogger(Neo4jTemplate.class); private final Infrastructure infrastructure; + private ApplicationContext applicationContext; /** * @param graphDatabase the neo4j graph database @@ -89,17 +92,17 @@ public class Neo4jTemplate implements Neo4jOperations { */ public Neo4jTemplate(final GraphDatabase graphDatabase, PlatformTransactionManager transactionManager) { notNull(graphDatabase, "graphDatabase"); - this.infrastructure = MappingInfrastructureFactoryBean.createDirect(graphDatabase,transactionManager); + this.infrastructure = MappingInfrastructureFactoryBean.createDirect(graphDatabase, transactionManager); } public Neo4jTemplate(final GraphDatabase graphDatabase) { notNull(graphDatabase, "graphDatabase"); - this.infrastructure = MappingInfrastructureFactoryBean.createDirect(graphDatabase,null); + this.infrastructure = MappingInfrastructureFactoryBean.createDirect(graphDatabase, null); } public Neo4jTemplate(final GraphDatabaseService graphDatabaseService) { notNull(graphDatabaseService, "graphDatabaseService"); - this.infrastructure = MappingInfrastructureFactoryBean.createDirect(graphDatabaseService,null); + this.infrastructure = MappingInfrastructureFactoryBean.createDirect(graphDatabaseService, null); } public Neo4jTemplate(Infrastructure infrastructure) { @@ -109,7 +112,7 @@ public Neo4jTemplate(Infrastructure infrastructure) { @Override public GraphRepository repositoryFor(Class clazz) { - notNull(clazz,"entity type"); + notNull(clazz, "entity type"); if (isNodeEntity(clazz)) return new NodeGraphRepositoryImpl(clazz, this); if (isRelationshipEntity(clazz)) return new RelationshipGraphRepository(clazz, this); throw new IllegalArgumentException("Can't create graph repository for non-graph entity of type " + clazz); @@ -143,12 +146,12 @@ public T findOne(long id, final Class entityClass) { final Neo4jPersistentEntityImpl persistentEntity = getPersistentEntity(entityClass); if (persistentEntity.isNodeEntity()) { final Node node = getNode(id); - if (node==null) return null; + if (node == null) return null; return infrastructure.getEntityPersister().createEntityFromState(node, entityClass, persistentEntity.getMappingPolicy(), this); } if (persistentEntity.isRelationshipEntity()) { final Relationship relationship = getRelationship(id); - if (relationship==null) return null; + if (relationship == null) return null; return infrastructure.getEntityPersister().createEntityFromState(relationship, entityClass, persistentEntity.getMappingPolicy(), this); } throw new IllegalArgumentException("provided entity type is neither annotated with @NodeEntiy nor @RelationshipEntity"); @@ -156,39 +159,41 @@ public T findOne(long id, final Class entityClass) { @Override public EndResult findAll(final Class entityClass) { - notNull(entityClass,"entity type"); + notNull(entityClass, "entity type"); final ClosableIterable all = infrastructure.getTypeRepresentationStrategies().findAll(getEntityType(entityClass)); return new QueryResultBuilder(all, getDefaultConverter()).to(entityClass); } @Override public long count(final Class entityClass) { - notNull(entityClass,"entity type"); + notNull(entityClass, "entity type"); return infrastructure.getTypeRepresentationStrategies().count(getEntityType(entityClass)); } public T createEntityFromStoredType(S state, MappingPolicy mappingPolicy) { - notNull(state,"node or relationship"); + notNull(state, "node or relationship"); return infrastructure.getEntityPersister().createEntityFromStoredType(state, mappingPolicy, this); } public T createEntityFromState(S state, Class type, MappingPolicy mappingPolicy) { - notNull(state,"node or relationship"); + notNull(state, "node or relationship"); return infrastructure.getEntityPersister().createEntityFromState(state, type, mappingPolicy, this); } + @Override public T load(S state, Class type) { - notNull(state,"node or relationship",type,"entity class"); + notNull(state, "node or relationship", type, "entity class"); return infrastructure.getEntityPersister().createEntityFromState(state, type, getMappingPolicy(type), this); } @Override public T projectTo(Object entity, Class targetType) { - notNull(entity,"entity",targetType,"new entity class"); + notNull(entity, "entity", targetType, "new entity class"); return infrastructure.getEntityPersister().projectTo(entity, targetType, this); } + public T projectTo(Object entity, Class targetType, MappingPolicy mappingPolicy) { - notNull(entity,"entity",targetType,"new entity class"); + notNull(entity, "entity", targetType, "new entity class"); return infrastructure.getEntityPersister().projectTo(entity, targetType, mappingPolicy, this); } @@ -214,8 +219,9 @@ public void postEntityCreation(S node, Class @Override public void delete(final Object entity) { - notNull(entity, "entity"); infrastructure.getEntityRemover().remove(entity); + + if (applicationContext != null) applicationContext.publishEvent(new DeleteEvent(this, entity)); } /** @@ -278,12 +284,15 @@ public boolean isRelationshipEntity(Class targetType) { @Override @SuppressWarnings("unchecked") public T save(T entity) { - return save( entity, null ); + return save(entity, null); } @SuppressWarnings("unchecked") - public T save( T entity, RelationshipType annotationProvidedRelationshipType ) { - return (T) infrastructure.getEntityPersister().persist(entity, getMappingPolicy(entity), this, annotationProvidedRelationshipType ); + public T save(T entity, final RelationshipType annotationProvidedRelationshipType) { + if (applicationContext != null) applicationContext.publishEvent(new BeforeSaveEvent(this, entity)); + T t = (T) infrastructure.getEntityPersister().persist(entity, getMappingPolicy(entity), this, annotationProvidedRelationshipType); + if (applicationContext != null) applicationContext.publishEvent(new AfterSaveEvent(this, entity)); + return t; } public boolean isManaged(Object entity) { @@ -311,10 +320,10 @@ public Object query(String statement, Map params, final TypeInfo @Override @SuppressWarnings("unchecked") public R getRelationshipBetween(Object start, Object end, Class relationshipEntityClass, String relationshipType) { - notNull(start,"start",end,"end",relationshipEntityClass,"relationshipEntityClass",relationshipType,"relationshipType"); + notNull(start, "start", end, "end", relationshipEntityClass, "relationshipEntityClass", relationshipType, "relationshipType"); final Relationship relationship = infrastructure.getEntityStateHandler().getRelationshipBetween(start, end, relationshipType); if (relationship == null) return null; - if (Relationship.class.isAssignableFrom(relationshipEntityClass)) return (R)relationship; + if (Relationship.class.isAssignableFrom(relationshipEntityClass)) return (R) relationship; final Neo4jPersistentEntityImpl persistentEntity = getPersistentEntity(relationshipEntityClass); return infrastructure.getEntityPersister().createEntityFromState(relationship, relationshipEntityClass, persistentEntity.getMappingPolicy(), this); } @@ -322,27 +331,28 @@ public R getRelationshipBetween(Object start, Object end, Class relations @Override @SuppressWarnings("unchecked") public Iterable getRelationshipsBetween(Object start, Object end, Class relationshipEntityClass, String relationshipType) { - notNull(start,"start",end,"end",relationshipEntityClass,"relationshipEntityClass",relationshipType,"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; + 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"); + notNull(start, "start", end, "end", relationshipType, "relationshipType"); return infrastructure.getEntityStateHandler().getRelationshipBetween(start, end, relationshipType); } + @Override public void deleteRelationshipBetween(Object start, Object end, String type) { - notNull(start,"start",end,"end",type,"relationshipType"); + notNull(start, "start", end, "end", type, "relationshipType"); infrastructure.getEntityRemover().removeRelationshipBetween(start, end, type); } @Override public R createRelationshipBetween(Object start, Object end, Class relationshipEntityClass, String relationshipType, boolean allowDuplicates) { - notNull(start,"start",end,"end",relationshipEntityClass,"relationshipEntityClass",relationshipType,"relationshipType"); + notNull(start, "start", end, "end", relationshipEntityClass, "relationshipEntityClass", relationshipType, "relationshipType"); final RelationshipResult result = infrastructure.getEntityStateHandler().createRelationshipBetween(start, end, relationshipType, allowDuplicates); if (result.type == RelationshipResult.Type.NEW) { // TODO @@ -364,7 +374,7 @@ public Relationship doWithGraph(GraphDatabase graph) throws Exception { @Override public Relationship getOrCreateRelationship(String indexName, String key, Object value, Node startNode, Node endNode, String type, Map properties) { - return getGraphDatabase().getOrCreateRelationship(indexName,key,value,startNode,endNode,type,properties); + return getGraphDatabase().getOrCreateRelationship(indexName, key, value, startNode, endNode, type, properties); } private final Neo4jExceptionTranslator exceptionTranslator = new Neo4jExceptionTranslator(); @@ -493,7 +503,7 @@ public T convert(Object value, Class type) { public ResultConverter getDefaultConverter() { final ResultConverter resultConverter = infrastructure.getResultConverter(); if (resultConverter instanceof Neo4jTemplateAware) { - return ((Neo4jTemplateAware)resultConverter).with(this); + return ((Neo4jTemplateAware) resultConverter).with(this); } return resultConverter; } @@ -507,7 +517,7 @@ public QueryEngine queryEngineFor(QueryType type) { @SuppressWarnings("unchecked") public Result> query(String statement, Map params) { notNull(statement, "statement"); - final QueryEngine> queryEngine = queryEngineFor(QueryType.Cypher); + final QueryEngine> queryEngine = queryEngineFor(QueryType.Cypher); return queryEngine.query(statement, params); } @@ -526,7 +536,7 @@ public Result traverse(Object start, TraversalDescription traversal) { @SuppressWarnings("unchecked") public Iterable traverse(Object entity, Class targetType, TraversalDescription traversalDescription) { - notNull(entity,"entity",targetType,"target type",traversalDescription,"traversal description"); + notNull(entity, "entity", targetType, "target type", traversalDescription, "traversal description"); return traverse(entity, traversalDescription).to((Class) targetType); } @@ -553,7 +563,7 @@ public Result lookup(String indexName, String f @Override public Result lookup(final Class indexedType, String propertyName, final Object value) { - notNull(propertyName, "property name", indexedType, "indexedType",value,"query value"); + notNull(propertyName, "property name", indexedType, "indexedType", value, "query value"); try { final Index index = getIndex(indexedType, propertyName); @@ -571,15 +581,15 @@ public Index getIndex(String indexName, Class Index getIndex(Class indexedType, String propertyName) { final Neo4jPersistentProperty property = getPersistentProperty(indexedType, propertyName); - if (property==null) return getIndexProvider().getIndex(indexedType,null); + if (property == null) return getIndexProvider().getIndex(indexedType, null); return getIndexProvider().getIndex(property, indexedType); } public Neo4jPersistentProperty getPersistentProperty(Class type, String propertyName) { - if (type==null || propertyName==null) return null; + if (type == null || propertyName == null) return null; final Neo4jPersistentEntityImpl persistentEntity = getPersistentEntity(type); final int dotIndex = propertyName.lastIndexOf("."); - if (dotIndex >-1) propertyName = propertyName.substring(dotIndex, propertyName.length()); + if (dotIndex > -1) propertyName = propertyName.substring(dotIndex, propertyName.length()); return persistentEntity.getPersistentProperty(propertyName); } @@ -635,27 +645,28 @@ public GraphDatabase getGraphDatabase() { public String getIndexKey(Neo4jPersistentProperty property) { return property.getIndexKey(); } + public Index getIndex(Neo4jPersistentProperty property, final Class instanceType) { return getIndexProvider().getIndex(property, instanceType); } public MappingPolicy getMappingPolicy(Object entity) { - ParameterCheck.notNull(entity,"entity"); - return getMappingPolicy( entity.getClass() ); + ParameterCheck.notNull(entity, "entity"); + return getMappingPolicy(entity.getClass()); } public StoredEntityType getStoredEntityType(Object entity) { - final PropertyContainer container = entity instanceof PropertyContainer ? (PropertyContainer)entity: getPersistentState(entity); - if (container==null) return null; + final PropertyContainer container = entity instanceof PropertyContainer ? (PropertyContainer) entity : getPersistentState(entity); + if (container == null) return null; final Object alias = getInfrastructure().getTypeRepresentationStrategies().readAliasFrom(container); return getMappingContext().getPersistentEntity(alias).getEntityType(); } @Override public Class getStoredJavaType(Object entity) { - final PropertyContainer container = entity instanceof PropertyContainer ? (PropertyContainer)entity: getPersistentState(entity); - if (container==null) return null; + final PropertyContainer container = entity instanceof PropertyContainer ? (PropertyContainer) entity : getPersistentState(entity); + if (container == null) return null; final Object alias = getInfrastructure().getTypeRepresentationStrategies().readAliasFrom(container); return getMappingContext().getPersistentEntity(alias).getType(); } @@ -668,9 +679,14 @@ public Node createUniqueNode(Object entity) { final Neo4jPersistentEntityImpl persistentEntity = getPersistentEntity(entity.getClass()); final Neo4jPersistentProperty uniqueProperty = persistentEntity.getUniqueProperty(); Object value = uniqueProperty.getValueFromEntity(entity, MappingPolicy.MAP_FIELD_DIRECT_POLICY); - if (value==null) return createNode(); + if (value == null) return createNode(); final IndexInfo indexInfo = uniqueProperty.getIndexInfo(); - if (value instanceof Number && indexInfo.isNumeric()) value=ValueContext.numeric((Number)value); + if (value instanceof Number && indexInfo.isNumeric()) value = ValueContext.numeric((Number) value); return getGraphDatabase().getOrCreateNode(indexInfo.getIndexName(), indexInfo.getIndexKey(), value, Collections.emptyMap()); } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/lifecycle/AfterSaveEventTests.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/lifecycle/AfterSaveEventTests.java new file mode 100644 index 0000000000..c24dea5228 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/lifecycle/AfterSaveEventTests.java @@ -0,0 +1,81 @@ +/** + * 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.lifecycle; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.test.ImpermanentGraphDatabase; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.annotation.GraphId; +import org.springframework.data.neo4j.annotation.NodeEntity; +import org.springframework.data.neo4j.config.EnableNeo4jRepositories; +import org.springframework.data.neo4j.config.Neo4jConfiguration; +import org.springframework.data.neo4j.support.Neo4jTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.neo4j.helpers.collection.IteratorUtil.single; + +@NodeEntity +class Bar { + @GraphId + Long id; + + String generatedId = "no event"; +} + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@Transactional +public class AfterSaveEventTests { + @Configuration + @EnableNeo4jRepositories + static class TestConfig extends Neo4jConfiguration { + @Bean + GraphDatabaseService graphDatabaseService() { + return new ImpermanentGraphDatabase(); + } + + @Bean + ApplicationListener beforeSaveEventApplicationListener() { + return new ApplicationListener() { + @Override + public void onApplicationEvent(AfterSaveEvent event) { + Bar bar = (Bar) event.getEntity(); + bar.generatedId = "after event"; + } + }; + } + } + + @Autowired + private Neo4jTemplate template; + + @Test + public void shouldFireAfterEntityIsSaved() throws Exception { + Bar entity = new Bar(); + assertThat(template.save(entity).generatedId, is("no event")); + assertThat(entity.generatedId, is("after event")); + assertThat(single(template.findAll(Bar.class)).generatedId, is("no event")); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/lifecycle/BeforeSaveEventTests.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/lifecycle/BeforeSaveEventTests.java new file mode 100644 index 0000000000..fc88f3e938 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/lifecycle/BeforeSaveEventTests.java @@ -0,0 +1,79 @@ +/** + * 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.lifecycle; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.test.ImpermanentGraphDatabase; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.annotation.GraphId; +import org.springframework.data.neo4j.annotation.NodeEntity; +import org.springframework.data.neo4j.config.EnableNeo4jRepositories; +import org.springframework.data.neo4j.config.Neo4jConfiguration; +import org.springframework.data.neo4j.support.Neo4jTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.neo4j.helpers.collection.IteratorUtil.single; + +@NodeEntity +class Foo { + @GraphId + Long id; + + String generatedId = "no event"; +} + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@Transactional +public class BeforeSaveEventTests { + @Configuration + @EnableNeo4jRepositories + static class TestConfig extends Neo4jConfiguration { + @Bean + GraphDatabaseService graphDatabaseService() { + return new ImpermanentGraphDatabase(); + } + + @Bean + ApplicationListener beforeSaveEventApplicationListener() { + return new ApplicationListener() { + @Override + public void onApplicationEvent(BeforeSaveEvent event) { + Foo foo = (Foo) event.getEntity(); + foo.generatedId = "before event"; + } + }; + } + } + + @Autowired + private Neo4jTemplate template; + + @Test + public void shouldFireBeforeEntityIsSaved() throws Exception { + assertThat(template.save(new Foo()).generatedId, is("before event")); + assertThat(single(template.findAll(Foo.class)).generatedId, is("before event")); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/lifecycle/DeleteEventTests.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/lifecycle/DeleteEventTests.java new file mode 100644 index 0000000000..ce2eb79bd1 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/lifecycle/DeleteEventTests.java @@ -0,0 +1,107 @@ +/** + * 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.lifecycle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.test.ImpermanentGraphDatabase; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.annotation.GraphId; +import org.springframework.data.neo4j.annotation.NodeEntity; +import org.springframework.data.neo4j.config.EnableNeo4jRepositories; +import org.springframework.data.neo4j.config.Neo4jConfiguration; +import org.springframework.data.neo4j.support.Neo4jTemplate; +import org.springframework.data.neo4j.support.node.Neo4jHelper; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.transaction.annotation.Transactional; + +import java.util.LinkedList; + +import static org.junit.Assert.assertThat; +import static org.junit.matchers.JUnitMatchers.hasItem; + +@NodeEntity +class Program { + @GraphId + Long id; + + String name; + + Program() { + } + + public Program(String name) { + this.name = name; + } +} + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@Transactional +public class DeleteEventTests { + @Configuration + @EnableNeo4jRepositories + static class TestConfig extends Neo4jConfiguration { + @Bean + GraphDatabaseService graphDatabaseService() { + return new ImpermanentGraphDatabase(); + } + + @Bean + ApplicationListener> deleteEventApplicationListener() { + return new ApplicationListener>() { + @Override + public void onApplicationEvent(DeleteEvent event) { + deletions.add(event.getEntity()); + } + }; + } + } + + @Autowired + Neo4jTemplate template; + + @Autowired + GraphDatabaseService graphDatabaseService; + + static final LinkedList deletions = new LinkedList(); + + @BeforeTransaction + public void beforeTransaction() { + Neo4jHelper.cleanDb(template); + } + + @Before + public void before() { + deletions.clear(); + } + + @Test + public void shouldFireEventOnNodeDeletion() throws Exception { + Program sark = template.save(new Program("Sark")); + + template.delete(sark); + + assertThat(deletions, hasItem(sark)); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/lifecycle/SaveEventTests.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/lifecycle/SaveEventTests.java new file mode 100644 index 0000000000..7f9ec541cd --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/lifecycle/SaveEventTests.java @@ -0,0 +1,332 @@ +/** + * 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.lifecycle; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.test.ImpermanentGraphDatabase; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.annotation.*; +import org.springframework.data.neo4j.config.EnableNeo4jRepositories; +import org.springframework.data.neo4j.config.Neo4jConfiguration; +import org.springframework.data.neo4j.support.Neo4jTemplate; +import org.springframework.data.neo4j.support.node.Neo4jHelper; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Set; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.junit.matchers.JUnitMatchers.either; +import static org.springframework.data.neo4j.SetHelper.asSet; + +@RelationshipEntity(type = "slew") +class Slaying { + @GraphId + Long id; + + @StartNode + FictionalCharacter slayor; + + @EndNode + FictionalCharacter slayee; + + String battle; + + Slaying() { + } + + Slaying(FictionalCharacter slayor, FictionalCharacter slayee, String battle) { + this.slayor = slayor; + this.slayee = slayee; + this.battle = battle; + } +} + + +@NodeEntity +class FictionalCharacter { + @GraphId + Long id; + + String name; + + @RelatedToVia + Slaying slew; + + @RelatedToVia(type = "slayered") + Set slayings; + + FictionalCharacter() { + } + + FictionalCharacter(String name) { + + this.name = name; + } + + void slew(FictionalCharacter fictionalCharacter, String battle) { + slew = new Slaying(this, fictionalCharacter, battle); + } + + void slew(FictionalCharacter... victims) { + slayings = new HashSet(); + + for (FictionalCharacter victim : victims) { + slayings.add(new Slaying(this, victim, null)); + } + } +} + +@NodeEntity +class Parent { + @GraphId + Long id; + + String name; + + @Fetch + Child eldest; + + @Fetch + Set youngsters; + + Parent() { + + } + + Parent(Child eldest) { + this.eldest = eldest; + } + + public Parent(String name) { + this.name = name; + } + + public Parent(Set youngsters) { + this.youngsters = youngsters; + } +} + +@NodeEntity +class Child { + @GraphId + Long id; + + String name; + + Child() { + } + + Child(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Child child = (Child) o; + + if (id != null ? !id.equals(child.id) : child.id != null) return false; + if (name != null ? !name.equals(child.name) : child.name != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = id != null ? id.hashCode() : 0; + result = 31 * result + (name != null ? name.hashCode() : 0); + return result; + } +} + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@Transactional +public class SaveEventTests { + @Configuration + @EnableNeo4jRepositories + static class TestConfig extends Neo4jConfiguration { + @Bean + GraphDatabaseService graphDatabaseService() { + return new ImpermanentGraphDatabase(); + } + + @Bean + ApplicationListener beforeSaveEventApplicationListener() { + return new ApplicationListener() { + @Override + public void onApplicationEvent(BeforeSaveEvent event) { + entities.add(event.getEntity()); + } + }; + } + } + + @Autowired + Neo4jTemplate template; + + static final LinkedList entities = new LinkedList(); + + @BeforeTransaction + public void beforeTransaction() { + Neo4jHelper.cleanDb(template); + } + + @Before + public void before() { + entities.clear(); + } + + @Test + public void shouldFireEventForNodeEntity() throws Exception { + Child child = new Child(); + template.save(child); + + assertThat(entities.size(), is(1)); + assertThat((Child) entities.get(0), is(child)); + } + + @Test + public void shouldFireEventForRelationshipEntity() throws Exception { + FictionalCharacter fingolfin = template.save(new FictionalCharacter("Fingolfin")); + FictionalCharacter morgoth = template.save(new FictionalCharacter("Morgoth")); + entities.clear(); + Slaying slaying = new Slaying(morgoth, fingolfin, "Dagor Bragollach"); + + template.save(slaying); + + assertThat(entities.size(), is(1)); + assertThat(((Slaying) entities.get(0)).battle, is(equalTo("Dagor Bragollach"))); + } + + @Test + public void shouldFireEventForNodeEntities() throws Exception { + Child child1 = new Child("Huey"); + Child child2 = new Child("Louie"); + Child child3 = new Child("Dewey"); + Parent parent = new Parent(asSet(child1, child2, child3)); + template.save(parent); + + assertThat(entities.size(), is(4)); + assertThat((Parent) entities.get(0), is(parent)); + assertThat((Child) entities.get(1), is(child1)); + assertThat((Child) entities.get(2), is(child2)); + assertThat((Child) entities.get(3), is(child3)); + } + + @Test + public void shouldFireEventForRelationshipEntities() throws Exception { + FictionalCharacter beleg = template.save(new FictionalCharacter("Beleg")); + FictionalCharacter brandir = template.save(new FictionalCharacter("Brandir")); + entities.clear(); + FictionalCharacter turin = new FictionalCharacter("Túrin"); + turin.slew(beleg, brandir); + + template.save(turin); + + assertThat(entities.size(), is(3)); + assertThat(((FictionalCharacter) entities.get(0)).id, is(turin.id)); + assertThat(((Slaying) entities.get(1)).slayee.id, is(either(equalTo(beleg.id)).or(equalTo(brandir.id)))); + assertThat(((Slaying) entities.get(2)).slayee.id, is(either(equalTo(beleg.id)).or(equalTo(brandir.id)))); + } + + @Test + public void shouldFireEventForNodeEntityWhenItIsSavedIndirectly() throws Exception { + Child child = new Child(); + Parent parent = new Parent(child); + template.save(parent); + + assertThat(entities.size(), is(2)); + assertThat((Parent) entities.get(0), is(parent)); + assertThat((Child) entities.get(1), is(child)); + } + + @Test + public void shouldFireEventForRelationshipEntityWhenItIsSavedIndirectly() throws Exception { + FictionalCharacter fingolfin = template.save(new FictionalCharacter("Fingolfin")); + entities.clear(); + FictionalCharacter morgoth = new FictionalCharacter("Morgoth"); + morgoth.slew(fingolfin, "Dagor Bragollach"); + + template.save(morgoth); + + assertThat(entities.size(), is(2)); + assertThat(((FictionalCharacter) entities.get(0)).id, is(morgoth.id)); + assertThat(((Slaying) entities.get(1)).battle, is(equalTo("Dagor Bragollach"))); + } + + @Test + public void shouldFireEventForUpdatedEntities() throws Exception { + Parent parent = template.save(new Parent("Donald Duck")); + entities.clear(); + parent = template.findOne(parent.id, Parent.class); + parent.name = "Mickey Mouse"; + + template.save(parent); + + assertThat(entities.size(), is(1)); + assertThat((Parent) entities.get(0), is(parent)); + } + + @Test + public void shouldFireEventEvenIfEntityHasNotBeenUpdated() throws Exception { + Parent parent = template.save(new Parent("Uncle Scrooge")); + entities.clear(); + parent = template.findOne(parent.id, Parent.class); + + template.save(parent); + + assertThat(entities.size(), is(1)); + assertThat((Parent) entities.get(0), is(parent)); + } + + /** + * because we do not save recursively, the child entity isn't saved and thus no event is fired + *

+ * so this is basically saying, "we do not save recursively" + *

+ * is that different in the advanced mapping mode case? + */ + @Test + public void shouldNotFireEventForUpdatedRelatedEntities() throws Exception { + Parent parent = template.save(new Parent(new Child("Daisy Duck"))); + entities.clear(); + parent = template.findOne(parent.id, Parent.class); + parent.eldest.name = "Minnie Mouse"; + + template.save(parent); + + assertThat(entities.size(), is(1)); + assertThat((Parent) entities.get(0), is(parent)); + } +} diff --git a/src/docbkx/reference/programming-model/template.xml b/src/docbkx/reference/programming-model/template.xml index 6cfed09b22..d5f9b7df01 100644 --- a/src/docbkx/reference/programming-model/template.xml +++ b/src/docbkx/reference/programming-model/template.xml @@ -107,4 +107,35 @@ via the graph database. +

+ Lifecycle Events + Neo4j Template offers basic lifecycle events via Spring's event mechanism using ApplicationListener and ApplicationEvent. The following hooks are available in the form of types of application event: + + BeforeSaveEvent + AfterSaveEvent + DeleteEvent - after the event has been deleted + + The following example demostrates how to register a listener and perform behaviour across types of entities + + Timestamping Entities + + beforeSaveEventApplicationListener() { + return new ApplicationListener() { + @Override + public void onApplicationEvent(BeforeSaveEvent event) { + Auditable entity = (Auditable) event.getEntity(); + entity.setLastUpdated(new Date()); + } + }; + } + ...]]> + + + Changes made to entities in the before-save event handler are reflected in the stored entity - after-save ones are not. +