DATAGRAPH-311 : Special Handling of managed type fields when serializing/deserializing

This commit is contained in:
Nicki Watt
2013-08-12 16:22:55 +01:00
parent bc5a2717c2
commit 764611cd35
7 changed files with 410 additions and 48 deletions

View File

@@ -17,12 +17,14 @@
package org.springframework.data.neo4j.fieldaccess;
import org.springframework.data.neo4j.core.EntityState;
import org.springframework.data.neo4j.mapping.MappingPolicy;
import org.springframework.data.neo4j.mapping.ManagedEntity;
import org.springframework.data.neo4j.mapping.MappingPolicy;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.support.DoReturn;
import org.springframework.data.neo4j.support.Neo4jTemplate;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.AbstractSet;
import java.util.Collection;
@@ -40,12 +42,20 @@ public class ManagedFieldAccessorSet<T> extends AbstractSet<T> implements Serial
private static final long serialVersionUID = 1L;
private final Object entity;
final Set<T> delegate;
private Object writeReplace() {
return new SerializationProxy<T>(this);
}
private void readObject(ObjectInputStream ois) throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
private final transient Object entity;
final transient Set<T> delegate;
private final transient Neo4jPersistentProperty property;
private final transient Neo4jTemplate ctx;
private final transient FieldAccessor fieldAccessor;
private final MappingPolicy mappingPolicy;
private final transient MappingPolicy mappingPolicy;
@SuppressWarnings("unchecked")
public ManagedFieldAccessorSet(final Object entity, final Object newVal, final Neo4jPersistentProperty property, Neo4jTemplate ctx, FieldAccessor fieldAccessor, final MappingPolicy mappingPolicy) {
@@ -149,4 +159,26 @@ public class ManagedFieldAccessorSet<T> extends AbstractSet<T> implements Serial
delegate.clear();
update();
}
}
/**
* Implementation of the Serialization Proxy Pattern (ref Item 78
* of Effective Java - 2nd edition)
* @param <T> Type of the underlying class being stored in the Set.
*/
private static class SerializationProxy<T> implements Serializable {
private static final long serialVersionUID = 1L;
private Set<T> delegateSet;
SerializationProxy(ManagedFieldAccessorSet<T> managedFieldAccessorSet) {
this.delegateSet = managedFieldAccessorSet.delegate;
}
private Object readResolve() {
return delegateSet;
}
}
}

View File

@@ -16,28 +16,40 @@
package org.springframework.data.neo4j.fieldaccess;
import org.springframework.data.neo4j.core.EntityState;
import org.springframework.data.neo4j.mapping.MappingPolicy;
import org.springframework.data.neo4j.mapping.ManagedEntity;
import org.springframework.data.neo4j.mapping.MappingPolicy;
import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.support.DoReturn;
import org.springframework.data.neo4j.support.Neo4jTemplate;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Map;
/**
* Updates the entity containing such a ManagedPrefixedDynamicProperties when some property is added, changed or
* deleted.
*/
public class ManagedPrefixedDynamicProperties extends PrefixedDynamicProperties {
public class ManagedPrefixedDynamicProperties extends PrefixedDynamicProperties implements Serializable
{
private static final long serialVersionUID = 1L;
private final Object entity;
private Object writeReplace() {
return new SerializationProxy(this);
}
private void readObject(ObjectInputStream ois) throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
private transient final Object entity;
private transient final Neo4jTemplate template;
private transient final FieldAccessor fieldAccessor;
private transient final Neo4jPersistentProperty property;
private boolean isNode;
private MappingPolicy mappingPolicy;
private transient boolean isNode;
private transient MappingPolicy mappingPolicy;
public ManagedPrefixedDynamicProperties(String prefix, final Neo4jPersistentProperty property, final Object entity, Neo4jTemplate template, FieldAccessor fieldAccessor, final MappingPolicy mappingPolicy) {
this(prefix,10,property,entity, template,fieldAccessor, mappingPolicy);
@@ -105,4 +117,28 @@ public class ManagedPrefixedDynamicProperties extends PrefixedDynamicProperties
property.setValue(entity, newValue);
return newValue;
}
/**
* Implementation of the Serialization Proxy Pattern (ref Item 78
* of Effective Java - 2nd edition)
* @param <T> Type of the underlying class being stored in the Set.
*/
private static class SerializationProxy<T> implements Serializable {
private static final long serialVersionUID = 1L;
private Map actualMapContent;
private String prefix;
SerializationProxy(ManagedPrefixedDynamicProperties prefixedDynamicProperties) {
this.actualMapContent = prefixedDynamicProperties.asMap();
this.prefix = prefixedDynamicProperties.prefix;
}
private Object readResolve() {
PrefixedDynamicProperties val = new PrefixedDynamicProperties(prefix);
val.setPropertiesFrom(actualMapContent);
return val;
}
}
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.neo4j.fieldaccess;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
@@ -32,8 +34,16 @@ public class PrefixedDynamicProperties implements DynamicProperties , Serializab
private static final long serialVersionUID = 1L;
private final Map<String, Object> map;
protected final String prefix;
private Object writeReplace() {
return new SerializationProxy(this);
}
private void readObject(ObjectInputStream ois) throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
private transient final Map<String, Object> map;
protected final transient String prefix;
/**
* Handles key prefixing
@@ -295,4 +305,28 @@ public class PrefixedDynamicProperties implements DynamicProperties , Serializab
}
return true;
}
/**
* Implementation of the Serialization Proxy Pattern (ref Item 78
* of Effective Java - 2nd edition)
* @param <T> Type of the underlying class being stored in the Set.
*/
private static class SerializationProxy<T> implements Serializable {
private static final long serialVersionUID = 1L;
private Map actualMapContent;
private String prefix;
SerializationProxy(PrefixedDynamicProperties prefixedDynamicProperties) {
this.actualMapContent = prefixedDynamicProperties.map;
this.prefix = prefixedDynamicProperties.prefix;
}
private Object readResolve() {
PrefixedDynamicProperties val = new PrefixedDynamicProperties(prefix);
val.setPropertiesFrom(actualMapContent);
return val;
}
}
}

View File

@@ -102,6 +102,9 @@ public class Person implements Being , Serializable {
@RelatedTo(elementClass = Group.class, type = "interface_test", direction = Direction.OUTGOING)
private Set<IGroup> groups;
@RelatedTo(elementClass = Person.class, type = "serialiation_test", direction = Direction.OUTGOING)
private Set<Person> serialFriends;
RootEntity root;
public RootEntity getRoot() {
@@ -313,4 +316,15 @@ public class Person implements Being , Serializable {
public BestFriend getBestFriend() {
return bestFriend;
}
public Set<Person> getSerialFriends() {
if (serialFriends == null) {
serialFriends = new HashSet<Person>();
}
return serialFriends;
}
public void addSerialFriend(Person serialFriend) {
getSerialFriends().add(serialFriend);
}
}

View File

@@ -0,0 +1,265 @@
/**
* 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.repository;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.fieldaccess.ManagedFieldAccessorSet;
import org.springframework.data.neo4j.fieldaccess.ManagedPrefixedDynamicProperties;
import org.springframework.data.neo4j.fieldaccess.PrefixedDynamicProperties;
import org.springframework.data.neo4j.model.Person;
import org.springframework.data.neo4j.support.Neo4jTemplate;
import org.springframework.data.neo4j.support.node.Neo4jHelper;
import org.springframework.test.context.CleanContextCacheTestExecutionListener;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.transaction.BeforeTransaction;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import java.io.*;
import java.util.Date;
import java.util.HashSet;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.neo4j.helpers.collection.IteratorUtil.asCollection;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@TestExecutionListeners({CleanContextCacheTestExecutionListener.class, DependencyInjectionTestExecutionListener.class, TransactionalTestExecutionListener.class})
public class SerializableEntityRepositoryTests {
protected final Logger log = LoggerFactory.getLogger(getClass());
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private Neo4jTemplate neo4jTemplate;
@Autowired
private PersonRepository personRepository;
@Autowired
GroupRepository groupRepository;
@Autowired
FriendshipRepository friendshipRepository;
private SerialTesters serialTesters;
private Date expectedBirthDate;
@BeforeTransaction
public void cleanDb() {
Neo4jHelper.cleanDb(neo4jTemplate);
}
@Before
public void setUp() throws Exception {
serialTesters = new SerialTesters();
serialTesters.createUpgraderTeam(personRepository, groupRepository, friendshipRepository);
expectedBirthDate = serialTesters.bdayFormatter.parse("01 JAN 2013 00:00:00");
}
@Test
public void shouldBeAbleToSerializeAndDeserializeBasicEntityGraph() throws Exception {
Person person = personRepository.findOne(serialTesters.nicki.getId());
assertEntityDetailsForPerson(person);
assertThat(person, instanceOf(Serializable.class));
// Do it
byte[] bos = serializeIt(person);
Person aDeserializedPerson = deserializeIt(bos);
// Verify its the same
assertEntityDetailsForPerson(aDeserializedPerson);
}
@Test
public void primitiveFieldShouldBeSerializedInOriginalForm() throws Exception {
Person deserializedPerson = assertPreSerializationSetupThenGetDeserializedPerson();
assertEquals(String.class, deserializedPerson.getName().getClass());
assertEquals("Nicki", deserializedPerson.getName());
}
@Test
public void primitiveFieldUpdatedOnDeserializedEntityShouldBeAbleToBeSavedBackToRepo() throws Exception {
final Person deserializedPerson = assertPreSerializationSetupThenGetDeserializedPerson();
assertEquals(String.class, deserializedPerson.getName().getClass());
assertEquals("Nicki", deserializedPerson.getName());
deserializedPerson.setName("New Name");
new TransactionTemplate(transactionManager).execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
personRepository.save(deserializedPerson);
}
});
Person personFromDB = personRepository.findOne(deserializedPerson.getId());
assertEquals("New Name", personFromDB.getName());
}
@Test
public void managedFieldAkaRelationshipsShouldBeSerializedAsAHashSet() throws Exception {
Person deserializedPerson = assertPreSerializationSetupThenGetDeserializedPerson();
assertEquals(HashSet.class, deserializedPerson.getSerialFriends().getClass());
assertEquals(1, deserializedPerson.getSerialFriends().size());
assertThat(deserializedPerson.getSerialFriends(), hasItem(serialTesters.michael));
}
@Test
public void managedFieldAkaRelationshipUpdatedOnDeserializedEntityShouldBeAbleToBeSavedBackToRepo() throws Exception {
final Person deserializedPerson = assertPreSerializationSetupThenGetDeserializedPerson();
assertEquals(HashSet.class, deserializedPerson.getSerialFriends().getClass());
assertEquals(1, deserializedPerson.getSerialFriends().size());
assertThat(deserializedPerson.getSerialFriends(), hasItem(serialTesters.michael));
deserializedPerson.addSerialFriend(serialTesters.david);
assertEquals(2, deserializedPerson.getSerialFriends().size());
new TransactionTemplate(transactionManager).execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
personRepository.save(deserializedPerson);
}
});
Person personFromDB = personRepository.findOne(deserializedPerson.getId());
assertEquals(2, personFromDB.getSerialFriends().size());
assertThat(personFromDB.getSerialFriends(), hasItem(serialTesters.michael));
assertThat(personFromDB.getSerialFriends(), hasItem(serialTesters.david));
}
@Test
public void dynamicPropertiesFieldShouldBeSerializedAsAPrefixedDynamicProperties() throws Exception {
final Person deserializedPerson = assertPreSerializationSetupThenGetDeserializedPerson();
assertEquals(PrefixedDynamicProperties.class, deserializedPerson.getPersonalProperties().getClass());
assertEquals(2, deserializedPerson.getPersonalProperties().asMap().size());
assertThat(asCollection( deserializedPerson.getPersonalProperties().getPropertyKeys()) , hasItems("addressLine1","addressLine2"));
}
@Test
public void dynamicPropertiesFieldUpdatedOnDeserializedEntityShouldBeAbleToBeSavedBackToRepo() throws Exception {
final Person deserializedPerson = assertPreSerializationSetupThenGetDeserializedPerson();
assertEquals(PrefixedDynamicProperties.class, deserializedPerson.getPersonalProperties().getClass());
assertEquals(2, deserializedPerson.getPersonalProperties().asMap().size());
assertThat(asCollection(deserializedPerson.getPersonalProperties().getPropertyKeys()) , hasItems("addressLine1", "addressLine2"));
deserializedPerson.setProperty("newDynoProp", "newDynoValue");
new TransactionTemplate(transactionManager).execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
personRepository.save(deserializedPerson);
}
});
Person personFromDB = personRepository.findOne(deserializedPerson.getId());
assertEquals(3, personFromDB.getPersonalProperties().asMap().size());
assertThat(asCollection(personFromDB.getPersonalProperties().getPropertyKeys()) , hasItems("addressLine1", "addressLine2", "newDynoProp"));
}
private Person assertPreSerializationSetupThenGetDeserializedPerson() throws Exception {
addSerialFriend(serialTesters.nicki.getId(), serialTesters.michael);
// 1A. Make sure that before we deal with any serialization, we are still operating
// with the expected ManagedFieldAccessorSet class
final Person person = personRepository.findOne(serialTesters.nicki.getId());
assertEquals(ManagedFieldAccessorSet.class, person.getSerialFriends().getClass());
assertEquals(1, person.getSerialFriends().size());
// 1B. Make sure that before we deal with any serialization, we are still operating
// with the expected ManagedPrefixedDynamicProperties class
assertEquals(ManagedPrefixedDynamicProperties.class, person.getPersonalProperties().getClass());
assertEquals(2, person.getPersonalProperties().asMap().size());
assertThat(asCollection( person.getPersonalProperties().getPropertyKeys()) , hasItems("addressLine1","addressLine2"));
// 2. Do Serialization and return serialized object
byte[] bos = serializeIt(person);
return deserializeIt(bos);
}
private void addSerialFriend(Long sourcePersonId, final Person target) {
final Person person1 = personRepository.findOne(sourcePersonId);
new TransactionTemplate(transactionManager).execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
person1.addSerialFriend(target);
personRepository.save(person1);
}
});
}
public void assertPOJOContainsExpectedData(MemberDataPOJO pojo) throws Exception {
assertNotNull(pojo);
assertThat(pojo.getBoss(), is(serialTesters.tareq));
assertThat(asCollection(pojo.getTeams()), hasItem(serialTesters.serialTesterGroup));
assertThat(pojo.getAnInt(), is(serialTesters.tareq.getAge()));
assertThat(pojo.getAName(), is(serialTesters.tareq.getName()));
}
private <T> byte[] serializeIt(T someObject) throws Exception {
ObjectOutputStream out = null;
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
out = new ObjectOutputStream(bos);
out.writeObject(someObject);
return bos.toByteArray();
} finally {
if (out != null) out.close();
}
}
private <T> T deserializeIt(byte[] serializedBytes) throws Exception {
ObjectInputStream in = null;
try {
ByteArrayInputStream bis = new ByteArrayInputStream(serializedBytes);
in = new ObjectInputStream(bis);
Object theNewObj = in.readObject();
return (T)theNewObj;
} finally {
if (in != null) in.close();
}
}
private void assertEntityDetailsForPerson(Person aPerson) {
assertThat(aPerson.getAge(), is(equalTo(36)));
assertThat(aPerson.getBoss(), is(serialTesters.tareq));
assertThat(aPerson.getBirthdate(), is(equalTo(expectedBirthDate)));
assertThat(aPerson.getName(), is(equalTo("Nicki")));
assertThat(aPerson.getDynamicProperty(), is(equalTo((Object)"What is this???")));
assertThat(aPerson.getFriendships(), hasItems(serialTesters.friendShip2, serialTesters.friendShip3)) ;
assertThat(aPerson.getHeight(), is(equalTo((short)100)));
assertThat(aPerson.getProperty("addressLine1"), is(equalTo((Object)"Somewhere")));
assertThat(aPerson.getProperty("addressLine2"), is(equalTo((Object)"Over the rainbow")));
}
}

View File

@@ -22,7 +22,6 @@ import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.model.Person;
import org.springframework.data.neo4j.support.Neo4jTemplate;
import org.springframework.data.neo4j.support.node.Neo4jHelper;
import org.springframework.test.context.CleanContextCacheTestExecutionListener;
@@ -35,7 +34,6 @@ import org.springframework.test.context.transaction.TransactionalTestExecutionLi
import org.springframework.transaction.annotation.Transactional;
import java.io.*;
import java.util.Date;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertNotNull;
@@ -62,8 +60,6 @@ public class SerializableGraphQueryRepositoryTests {
FriendshipRepository friendshipRepository;
private SerialTesters serialTesters;
private Date expectedBirthDate;
@BeforeTransaction
public void cleanDb() {
@@ -74,41 +70,21 @@ public class SerializableGraphQueryRepositoryTests {
public void setUp() throws Exception {
serialTesters = new SerialTesters();
serialTesters.createUpgraderTeam(personRepository, groupRepository, friendshipRepository);
expectedBirthDate = serialTesters.bdayFormatter.parse("01 JAN 2013 00:00:00");
}
@Test @Transactional
public void shouldBeAbleToTurnQueryResultsIntoAPOJO() throws Exception {
public void shouldBeAbleToTurnQueryResultIntoAPOJO() throws Exception {
MemberDataPOJO nickisMemberData = personRepository.findMemberDataPojo(serialTesters.nicki);
assertPOJOContainsExpectedData(nickisMemberData);
}
@Test @Transactional
public void shouldBeAbleToSerializeAndDeserializeEntity() throws Exception {
Person anSDNUpgrader = personRepository.findOne(serialTesters.nicki.getId());
assertEntityDetailsForPerson(anSDNUpgrader);
Person aDeserializedSDNUpgrader = assertObjectCanBeSerializedAndDeserialized(anSDNUpgrader);
assertEntityDetailsForPerson(aDeserializedSDNUpgrader);
}
private void assertEntityDetailsForPerson(Person aPerson) {
assertThat(aPerson.getAge(), is(equalTo(36)));
assertThat(aPerson.getBoss(), is(serialTesters.tareq));
assertThat(aPerson.getBirthdate(), is(equalTo(expectedBirthDate)));
assertThat(aPerson.getName(), is(equalTo("Nicki")));
assertThat(aPerson.getDynamicProperty(), is(equalTo((Object)"What is this???")));
assertThat(aPerson.getFriendships(), hasItems(serialTesters.friendShip2, serialTesters.friendShip3)) ;
assertThat(aPerson.getHeight(), is(equalTo((short)100)));
assertThat(aPerson.getProperty("addressLine1"), is(equalTo((Object)"Somewhere")));
assertThat(aPerson.getProperty("addressLine2"), is(equalTo((Object)"Over the rainbow")));
}
@Test @Transactional
public void shouldBeAbleToSerializedPOJOReturnedFromQueryResult() throws Exception {
MemberDataPOJO nickisOrigMemberData = personRepository.findMemberDataPojo(serialTesters.nicki);
assertPOJOContainsExpectedData(nickisOrigMemberData);
MemberDataPOJO nickisDeserMemberData = assertObjectCanBeSerializedAndDeserialized(nickisOrigMemberData);
assertThat(nickisOrigMemberData, instanceOf(Serializable.class));
byte[] bos = serializeIt(nickisOrigMemberData);
MemberDataPOJO nickisDeserMemberData = deserializeIt(bos);
assertPOJOContainsExpectedData(nickisDeserMemberData);
}
@@ -120,12 +96,6 @@ public class SerializableGraphQueryRepositoryTests {
assertThat(pojo.getAName(), is(serialTesters.tareq.getName()));
}
private <T> T assertObjectCanBeSerializedAndDeserialized(T someObject) throws Exception {
assertThat(someObject, instanceOf(Serializable.class));
byte[] bos = serializeIt(someObject);
return deserializeIt(bos);
}
private <T> byte[] serializeIt(T someObject) throws Exception {
ObjectOutputStream out = null;
try {
@@ -151,6 +121,4 @@ public class SerializableGraphQueryRepositoryTests {
}
}

View File

@@ -0,0 +1,13 @@
<?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"
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">
<context:annotation-config/>
<neo4j:config graphDatabaseService="graphDatabaseService"/>
<neo4j:repositories base-package="org.springframework.data.neo4j.repository"/>
<bean id="graphDatabaseService" class="org.neo4j.test.ImpermanentGraphDatabase" destroy-method="shutdown"/>
</beans>