Implemented a type safety policy to allow type safe usage of repositories' find methods. Three settings are supported:

NONE: No type safety is enforced on repository level. Thus the behaviour is the same as in past release. This is the default if the user selects no other policy via the Spring application context configuration.

RETURNS_NULL: If an entity with a given ID is loaded, but is from a different type as requested null is returned.

THROWS_EXCEPTION: If an entity with a given ID is loaded, but is from a different type as requested a InvalidEntityTypeException is thrown.
The policy can be selected by defining a Spring bean which is then autowired in the Neo config.

The policy is set by providing a bean with ID "typeSafetyPolicy" in the Spring application context configuration.

See http://stackoverflow.com/questions/13363607/resolving-entities-with-spring-data-neo4j-returns-wrong-entity-types/13375463#13375463 for a discussion concerning this.
This commit is contained in:
Sebastian Pätzold
2013-06-03 15:23:27 +02:00
parent 2d2ba2334f
commit 19e3529ed7
11 changed files with 295 additions and 6 deletions

View File

@@ -56,6 +56,7 @@ import org.springframework.data.neo4j.support.relationship.RelationshipEntityIns
import org.springframework.data.neo4j.support.relationship.RelationshipEntityStateFactory;
import org.springframework.data.neo4j.support.typerepresentation.ClassValueTypeInformationMapper;
import org.springframework.data.neo4j.support.typerepresentation.TypeRepresentationStrategyFactory;
import org.springframework.data.neo4j.support.typesafety.TypeSafetyPolicy;
import org.springframework.data.support.IsNewStrategyFactory;
import org.springframework.transaction.PlatformTransactionManager;
import javax.validation.Validator;
@@ -117,6 +118,7 @@ public abstract class Neo4jConfiguration {
factoryBean.setTransactionManager(neo4jTransactionManager());
factoryBean.setGraphDatabase(graphDatabase());
factoryBean.setIsNewStrategyFactory(isNewStrategyFactory());
factoryBean.setTypeSafetyPolicy(typeSafetyPolicy());
factoryBean.setIndexProvider(indexProvider());
@@ -269,6 +271,11 @@ public abstract class Neo4jConfiguration {
return new IndexProviderImpl(neo4jMappingContext(), graphDatabase());
}
@Bean
public TypeSafetyPolicy typeSafetyPolicy() throws Exception {
return new TypeSafetyPolicy();
}
public Set<? extends Class<?>> getInitialEntitySet() {
return initialEntitySet;
}

View File

@@ -32,6 +32,7 @@ import org.springframework.data.neo4j.support.node.EntityStateFactory;
import org.springframework.data.neo4j.support.query.CypherQueryExecutor;
import org.springframework.data.neo4j.support.typerepresentation.TypeRepresentationStrategies;
import org.springframework.data.neo4j.support.typerepresentation.TypeRepresentationStrategyFactory;
import org.springframework.data.neo4j.support.typesafety.TypeSafetyPolicy;
import org.springframework.transaction.PlatformTransactionManager;
import javax.validation.Validator;
@@ -68,4 +69,6 @@ public interface Infrastructure {
TypeRepresentationStrategy<Node> getNodeTypeRepresentationStrategy();
TypeRepresentationStrategy<Relationship> getRelationshipTypeRepresentationStrategy();
TypeSafetyPolicy getTypeSafetyPolicy();
}

View File

@@ -32,6 +32,7 @@ import org.springframework.data.neo4j.support.node.EntityStateFactory;
import org.springframework.data.neo4j.support.query.CypherQueryExecutor;
import org.springframework.data.neo4j.support.typerepresentation.TypeRepresentationStrategies;
import org.springframework.data.neo4j.support.typerepresentation.TypeRepresentationStrategyFactory;
import org.springframework.data.neo4j.support.typesafety.TypeSafetyPolicy;
import org.springframework.transaction.PlatformTransactionManager;
import javax.validation.Validator;
@@ -59,8 +60,9 @@ public class MappingInfrastructure implements Infrastructure {
private final IndexProvider indexProvider;
private final GraphDatabaseService graphDatabaseService;
private final GraphDatabase graphDatabase;
private final TypeSafetyPolicy typeSafetyPolicy;
public MappingInfrastructure(GraphDatabase graphDatabase, GraphDatabaseService graphDatabaseService, IndexProvider indexProvider, ResultConverter resultConverter, PlatformTransactionManager transactionManager, TypeRepresentationStrategies typeRepresentationStrategies, EntityRemover entityRemover, Neo4jEntityPersister entityPersister, EntityStateHandler entityStateHandler, CypherQueryExecutor cypherQueryExecutor, Neo4jMappingContext mappingContext, TypeRepresentationStrategy<Relationship> relationshipTypeRepresentationStrategy, TypeRepresentationStrategy<Node> nodeTypeRepresentationStrategy, Validator validator, ConversionService conversionService) {
public MappingInfrastructure(GraphDatabase graphDatabase, GraphDatabaseService graphDatabaseService, IndexProvider indexProvider, ResultConverter resultConverter, PlatformTransactionManager transactionManager, TypeRepresentationStrategies typeRepresentationStrategies, EntityRemover entityRemover, Neo4jEntityPersister entityPersister, EntityStateHandler entityStateHandler, CypherQueryExecutor cypherQueryExecutor, Neo4jMappingContext mappingContext, TypeRepresentationStrategy<Relationship> relationshipTypeRepresentationStrategy, TypeRepresentationStrategy<Node> nodeTypeRepresentationStrategy, Validator validator, ConversionService conversionService, TypeSafetyPolicy typeSafetyPolicy) {
this.graphDatabase = graphDatabase;
this.graphDatabaseService = graphDatabaseService;
this.indexProvider = indexProvider;
@@ -76,6 +78,7 @@ public class MappingInfrastructure implements Infrastructure {
this.nodeTypeRepresentationStrategy = nodeTypeRepresentationStrategy;
this.validator = validator;
this.conversionService = conversionService;
this.typeSafetyPolicy = typeSafetyPolicy;
}
@Override
@@ -147,4 +150,9 @@ public class MappingInfrastructure implements Infrastructure {
public TypeRepresentationStrategy<Relationship> getRelationshipTypeRepresentationStrategy() {
return relationshipTypeRepresentationStrategy;
}
@Override
public TypeSafetyPolicy getTypeSafetyPolicy() {
return typeSafetyPolicy;
}
}

View File

@@ -47,6 +47,7 @@ import org.springframework.data.neo4j.support.relationship.RelationshipEntityIns
import org.springframework.data.neo4j.support.relationship.RelationshipEntityStateFactory;
import org.springframework.data.neo4j.support.typerepresentation.TypeRepresentationStrategies;
import org.springframework.data.neo4j.support.typerepresentation.TypeRepresentationStrategyFactory;
import org.springframework.data.neo4j.support.typesafety.TypeSafetyPolicy;
import org.springframework.data.support.IsNewStrategyFactory;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.jta.JtaTransactionManager;
@@ -80,6 +81,7 @@ public class MappingInfrastructureFactoryBean implements FactoryBean<Infrastruct
private GraphDatabaseService graphDatabaseService;
private GraphDatabase graphDatabase;
private IsNewStrategyFactory isNewStrategyFactory;
private TypeSafetyPolicy typeSafetyPolicy;
private MappingInfrastructure mappingInfrastructure;
private TypeRepresentationStrategyFactory.Strategy typeRepresentationStrategy;
@@ -154,7 +156,10 @@ public class MappingInfrastructureFactoryBean implements FactoryBean<Infrastruct
if (this.indexProvider == null) {
this.indexProvider = new IndexProviderImpl(this.mappingContext, graphDatabase);
}
this.mappingInfrastructure = new MappingInfrastructure(graphDatabase, graphDatabaseService, indexProvider, resultConverter, transactionManager, typeRepresentationStrategies, entityRemover, entityPersister, entityStateHandler, cypherQueryExecutor, mappingContext, relationshipTypeRepresentationStrategy, nodeTypeRepresentationStrategy, validator, conversionService);
if (this.typeSafetyPolicy == null) {
this.typeSafetyPolicy = new TypeSafetyPolicy();
}
this.mappingInfrastructure = new MappingInfrastructure(graphDatabase, graphDatabaseService, indexProvider, resultConverter, transactionManager, typeRepresentationStrategies, entityRemover, entityPersister, entityStateHandler, cypherQueryExecutor, mappingContext, relationshipTypeRepresentationStrategy, nodeTypeRepresentationStrategy, validator, conversionService, typeSafetyPolicy);
} catch (Exception e) {
throw new RuntimeException("error initializing "+getClass().getName(),e);
}
@@ -294,6 +299,14 @@ public class MappingInfrastructureFactoryBean implements FactoryBean<Infrastruct
isNewStrategyFactory = newStrategyFactory;
}
public void setTypeSafetyPolicy(TypeSafetyPolicy typeSafetyPolicy) {
this.typeSafetyPolicy = typeSafetyPolicy;
}
public TypeSafetyPolicy getTypeSafetyPolicy() {
return typeSafetyPolicy;
}
@Override
public Infrastructure getObject() {
return mappingInfrastructure;

View File

@@ -27,6 +27,8 @@ import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.neo4j.mapping.*;
import org.springframework.data.neo4j.mapping.ManagedEntity;
import org.springframework.data.neo4j.support.Neo4jTemplate;
import org.springframework.data.neo4j.support.typesafety.TypeSafetyOption;
import org.springframework.data.neo4j.support.typesafety.TypeSafetyPolicy;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
@@ -78,14 +80,24 @@ public class Neo4jEntityConverterImpl<T,S extends PropertyContainer> implements
// retrieve meta-information about the type
@SuppressWarnings("unchecked") final Neo4jPersistentEntityImpl<R> persistentEntity = (Neo4jPersistentEntityImpl<R>) mappingContext.getPersistentEntity(targetType);
if (mappingPolicy==null) {
mappingPolicy = persistentEntity.getMappingPolicy();
// 4) check type safety
TypeSafetyPolicy typeSafetyPolicy = template.getInfrastructure().getTypeSafetyPolicy();
if (typeSafetyPolicy.isTypeSafetyEnabled() && !storedAndRequestedTypesMatch(requestedType, source)) {
if (typeSafetyPolicy.getTypeSafetyOption() == TypeSafetyOption.RETURNS_NULL) {
return null;
}
if (typeSafetyPolicy.getTypeSafetyOption() == TypeSafetyOption.THROWS_EXCEPTION) {
throw new InvalidEntityTypeException("Requested a entity of type '" + requestedType + "', but the stored entity is of type '" + typeMapper.readType(source).getType() + "'.");
}
}
// 4) create object instance
// 5) create object instance
if (mappingPolicy == null) {
mappingPolicy = persistentEntity.getMappingPolicy();
}
final R createdEntity = entityInstantiator.createEntityFromState(source, targetType.getType(), mappingPolicy);
// 5) connect state
// 6) connect state
entityStateHandler.setPersistentState(createdEntity,source);
if (persistentEntity.isManaged()) return createdEntity;
@@ -104,6 +116,11 @@ public class Neo4jEntityConverterImpl<T,S extends PropertyContainer> implements
return entity;
}
private <R extends T> boolean storedAndRequestedTypesMatch(Class<R> requestedType, S source) {
TypeInformation<?> storedType = typeMapper.readType(source);
return storedType.getType().isAssignableFrom(requestedType);
}
private <R extends T> void cascadeFetch(Neo4jPersistentEntityImpl<R> persistentEntity, final BeanWrapper<Neo4jPersistentEntity<R>, R> wrapper, final MappingPolicy policy, final Neo4jTemplate template) {
persistentEntity.doWithAssociations(new AssociationHandler<Neo4jPersistentProperty>() {
@Override

View File

@@ -0,0 +1,19 @@
package org.springframework.data.neo4j.support.typesafety;
/**
* Enum of all type safety options which are available in the system.
*
* @author spaetzold
*/
public enum TypeSafetyOption {
/** Sets the system to not be type safe (default setting). */
NONE,
/** Sets the system to return null if a entity should be loaded which is not of the requested type. */
RETURNS_NULL,
/** Sets the system to throw an exception if a entity should be loaded which is not of the requested type. */
THROWS_EXCEPTION
}

View File

@@ -0,0 +1,49 @@
package org.springframework.data.neo4j.support.typesafety;
import org.springframework.data.neo4j.support.ParameterCheck;
/**
* The type safety policy describes how repositories behave when loading entities from Neo4j.
*
* @author spaetzold
*/
public class TypeSafetyPolicy {
private final TypeSafetyOption option;
/**
* Creates a new instance of a type safety policy with the desired type safety option.
*
* @param option the type safety option
*/
public TypeSafetyPolicy(TypeSafetyOption option) {
ParameterCheck.notNull(option, "option");
this.option = option;
}
/**
* Creates a new instance of a type safety policy with no type safety enabled.
*/
public TypeSafetyPolicy() {
this.option = TypeSafetyOption.NONE;
}
/**
* Indicates if type safety is enabled in the system.
*
* @return true if type safety is enabled, else false
*/
public boolean isTypeSafetyEnabled() {
return option != TypeSafetyOption.NONE;
}
/**
* Gets the currently defined system type safety option.
*
* @return the type safety option
*/
public TypeSafetyOption getTypeSafetyOption() {
return option;
}
}

View File

@@ -0,0 +1,67 @@
/**
* 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.support;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.neo4j.graphdb.NotFoundException;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.data.neo4j.mapping.InvalidEntityTypeException;
import org.springframework.data.neo4j.model.Group;
import org.springframework.data.neo4j.model.Person;
import org.springframework.data.neo4j.template.Neo4jOperations;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
/**
* @author spaetzold
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:template-config-context-with-type-safety-throwing-exception.xml"})
public class ExceptionThrowingTypeSafetyNeo4jTemplateTests extends EntityTestBase {
private Neo4jOperations neo4jOperations;
@Before
public void setUp() throws Exception {
createTeam();
neo4jOperations = template;
}
@Test(expected = InvalidEntityTypeException.class)
@Transactional
public void testFindOneWithWrongTypeThrowsInvalidEntityTypeException() throws Exception {
neo4jOperations.findOne(testTeam.michael.getId(), Group.class);
}
@Test
@Transactional
public void testFindOneWithRightTypeReturnsPerson() throws Exception {
final Person found = neo4jOperations.findOne(testTeam.michael.getId(), Person.class);
assertNotNull(found);
}
@Test(expected = DataRetrievalFailureException.class)
@Transactional
public void testFindOneWithNonExistingIdThrowsDataRetrievalFailureException() throws Exception {
neo4jOperations.findOne(Long.MAX_VALUE, Person.class);
}
}

View File

@@ -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.support;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.neo4j.graphdb.NotFoundException;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.data.neo4j.mapping.InvalidEntityTypeException;
import org.springframework.data.neo4j.model.Group;
import org.springframework.data.neo4j.model.Person;
import org.springframework.data.neo4j.template.Neo4jOperations;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
/**
* @author spaetzold
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:template-config-context-with-type-safety-returning-null.xml"})
public class NullReturningTypeSafetyNeo4jTemplateTests extends EntityTestBase {
private Neo4jOperations neo4jOperations;
@Before
public void setUp() throws Exception {
createTeam();
neo4jOperations = template;
}
@Test
@Transactional
public void testFindOneWithWrongTypeReturnsNull() throws Exception {
final Group found = neo4jOperations.findOne(testTeam.michael.getId(), Group.class);
assertNull(found);
}
@Test
@Transactional
public void testFindOneWithRightTypeReturnsPerson() throws Exception {
final Person found = neo4jOperations.findOne(testTeam.michael.getId(), Person.class);
assertNotNull(found);
}
@Test(expected = DataRetrievalFailureException.class)
@Transactional
public void testFindOneWithNonExistingIdThrowsDataRetrievalFailureException() throws Exception {
neo4jOperations.findOne(Long.MAX_VALUE, Person.class);
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:neo4j="http://www.springframework.org/schema/data/neo4j"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/data/neo4j http://www.springframework.org/schema/data/neo4j/spring-neo4j.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<context:annotation-config/>
<neo4j:config graphDatabaseService="graphDatabaseService"/>
<neo4j:repositories base-package="org.springframework.data.neo4j.model"/>
<bean id="graphDatabaseService" class="org.neo4j.test.ImpermanentGraphDatabase" destroy-method="shutdown"/>
<bean id="typeSafetyPolicy" class="org.springframework.data.neo4j.support.typesafety.TypeSafetyPolicy">
<constructor-arg type="org.springframework.data.neo4j.support.typesafety.TypeSafetyOption"><value>RETURNS_NULL</value></constructor-arg>
</bean>
<tx:annotation-driven mode="aspectj"/>
</beans>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:neo4j="http://www.springframework.org/schema/data/neo4j"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/data/neo4j http://www.springframework.org/schema/data/neo4j/spring-neo4j.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<context:annotation-config/>
<neo4j:config graphDatabaseService="graphDatabaseService"/>
<neo4j:repositories base-package="org.springframework.data.neo4j.model"/>
<bean id="graphDatabaseService" class="org.neo4j.test.ImpermanentGraphDatabase" destroy-method="shutdown"/>
<bean id="typeSafetyPolicy" class="org.springframework.data.neo4j.support.typesafety.TypeSafetyPolicy">
<constructor-arg type="org.springframework.data.neo4j.support.typesafety.TypeSafetyOption"><value>THROWS_EXCEPTION</value></constructor-arg>
</bean>
<tx:annotation-driven mode="aspectj"/>
</beans>