From 94684c6aba6c603dd6d89ea5bfb405dacd3496f8 Mon Sep 17 00:00:00 2001 From: Michael Hunger Date: Sat, 5 Nov 2011 07:00:14 +0100 Subject: [PATCH] added support for arrays, iterables, collections, sets and lists of primitive and convertible values as node properties --- .../data/neo4j/aspects/Group.java | 77 +++++++++++++++ .../neo4j/aspects/support/NodeEntityTest.java | 82 +++++++++++++++- spring-data-neo4j-rest/pom.xml | 2 +- ...rtingNodePropertyFieldAccessorFactory.java | 20 ++-- .../neo4j/fieldaccess/PropertyConverter.java | 95 +++++++++++++++++++ .../PropertyFieldAccessorFactory.java | 12 ++- .../mapping/Neo4jPersistentProperty.java | 6 +- .../mapping/Neo4JPersistentPropertyImpl.java | 16 ++-- 8 files changed, 282 insertions(+), 28 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/PropertyConverter.java 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 0871108cb..049f83f3c 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 @@ -29,6 +29,7 @@ import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty; import org.springframework.data.neo4j.support.index.IndexType; import java.util.Collection; +import java.util.Set; @NodeEntity public class Group { @@ -79,6 +80,7 @@ public class Group { @Indexed(level=Indexed.Level.INSTANCE) private String indexLevelName; + private String[] roleNames; public String getFullTextName() { return fullTextName; @@ -124,6 +126,33 @@ public class Group { this.otherName = otherName; } + public void setRoleNames(String...roleNames) { + this.roleNames=roleNames; + } + + public String[] getRoleNames() { + return roleNames; + } + + public enum Role { ADMIN, USER } + Role[] roles; + + public Role[] getRoles() { + return roles; + } + + public void setRoles(Role...roles) { + this.roles = roles; + } + + Collection roleNamesColl; + Collection rolesColl; + Set roleNamesSet; + Set rolesSet; + Iterable roleNamesIterable; + Iterable rolesIterable; + + private static class PeopleTraversalBuilder implements FieldTraversalDescriptionBuilder { @Override public TraversalDescription build(Object start, Neo4jPersistentProperty property, String...params) { @@ -177,4 +206,52 @@ public class Group { public Iterable getPeopleRelationships() { return peopleRelationships; } + + public Collection getRoleNamesColl() { + return roleNamesColl; + } + + public void setRoleNamesColl(Collection roleNamesColl) { + this.roleNamesColl = roleNamesColl; + } + + public Collection getRolesColl() { + return rolesColl; + } + + public void setRolesColl(Collection rolesColl) { + this.rolesColl = rolesColl; + } + + public Set getRoleNamesSet() { + return roleNamesSet; + } + + public void setRoleNamesSet(Set roleNamesSet) { + this.roleNamesSet = roleNamesSet; + } + + public Set getRolesSet() { + return rolesSet; + } + + public void setRolesSet(Set rolesSet) { + this.rolesSet = rolesSet; + } + + public Iterable getRoleNamesIterable() { + return roleNamesIterable; + } + + public void setRoleNamesIterable(Iterable roleNamesIterable) { + this.roleNamesIterable = roleNamesIterable; + } + + public Iterable getRolesIterable() { + return rolesIterable; + } + + public void setRolesIterable(Iterable rolesIterable) { + this.rolesIterable = rolesIterable; + } } diff --git a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/NodeEntityTest.java b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/NodeEntityTest.java index b0672d3af..32ff87038 100644 --- a/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/NodeEntityTest.java +++ b/spring-data-neo4j-aspects/src/test/java/org/springframework/data/neo4j/aspects/support/NodeEntityTest.java @@ -28,6 +28,12 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.springframework.data.neo4j.aspects.Person.persistedPerson; @@ -49,7 +55,7 @@ public class NodeEntityTest extends EntityTestBase { @Test @Transactional - public void testSetProperties() { + public void testSetSimpleProperties() { String name = "Michael"; int age = 35; short height = 182; @@ -63,6 +69,80 @@ public class NodeEntityTest extends EntityTestBase { assertEquals((Short)height, p.getHeight()); } + @Test + @Transactional + public void testArrayProperties() { + Group g = new Group().persist(); + final String[] roleNames = {"a", "b", "c"}; + g.setRoleNames(roleNames); + assertArrayEquals(roleNames, (String[])getNodeState(g).getProperty("roleNames")); + assertArrayEquals(roleNames, g.getRoleNames()); + } + + @Test + @Transactional + public void testConvertedArrayProperties() { + Group g = new Group().persist(); + g.setRoles(Group.Role.values()); + assertArrayEquals(new String[] {"ADMIN","USER"}, (String[])getNodeState(g).getProperty("roles")); + assertArrayEquals(Group.Role.values(), g.getRoles()); + } + @Test + @Transactional + public void testCollectionProperties() { + Group g = new Group().persist(); + final List roleNames = asList("a", "b", "c"); + g.setRoleNamesColl(roleNames); + assertArrayEquals(roleNames.toArray(), (String[])getNodeState(g).getProperty("roleNamesColl")); + assertEquals(roleNames, g.getRoleNamesColl()); + } + + @Test + @Transactional + public void testConvertedCollectionProperties() { + Group g = new Group().persist(); + g.setRolesColl(asList(Group.Role.values())); + assertArrayEquals(new String[] {"ADMIN","USER"}, (String[])getNodeState(g).getProperty("rolesColl")); + assertEquals(asList(Group.Role.values()), g.getRolesColl()); + } + @Test + @Transactional + public void testIterableProperties() { + Group g = new Group().persist(); + final List roleNames = asList("a", "b", "c"); + g.setRoleNamesIterable(roleNames); + assertArrayEquals(roleNames.toArray(), (String[])getNodeState(g).getProperty("roleNamesIterable")); + assertEquals(roleNames, g.getRoleNamesIterable()); + } + + @Test + @Transactional + public void testConvertedIterableProperties() { + Group g = new Group().persist(); + g.setRolesIterable(asList(Group.Role.values())); + assertArrayEquals(new String[] {"ADMIN","USER"}, (String[])getNodeState(g).getProperty("rolesIterable")); + assertEquals(asList(Group.Role.values()), g.getRolesIterable()); + } + @Test + @Transactional + public void testSetProperties() { + Group g = new Group().persist(); + final Set roleNames = new LinkedHashSet(asList("a", "b", "c")); + g.setRoleNamesSet(roleNames); + assertArrayEquals(roleNames.toArray(), (String[])getNodeState(g).getProperty("roleNamesSet")); + assertEquals(roleNames, g.getRoleNamesSet()); + } + + @Test + @Transactional + public void testConvertedSetProperties() { + Group g = new Group().persist(); + final LinkedHashSet roles = new LinkedHashSet(asList(Group.Role.values())); + g.setRolesSet(roles); + assertArrayEquals(new String[] {"ADMIN","USER"}, (String[])getNodeState(g).getProperty("rolesSet")); + assertEquals(roles, g.getRolesSet()); + } + @Test @Transactional public void testSetShortProperty() { diff --git a/spring-data-neo4j-rest/pom.xml b/spring-data-neo4j-rest/pom.xml index b8825a48e..614969240 100644 --- a/spring-data-neo4j-rest/pom.xml +++ b/spring-data-neo4j-rest/pom.xml @@ -76,7 +76,7 @@ org.neo4j neo4j-rest-graphdb - 1.5.M02.U1 + 1.5-SNAPSHOT org.neo4j diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/ConvertingNodePropertyFieldAccessorFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/ConvertingNodePropertyFieldAccessorFactory.java index 5c8758cac..c70fc52d2 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/ConvertingNodePropertyFieldAccessorFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/ConvertingNodePropertyFieldAccessorFactory.java @@ -18,7 +18,6 @@ package org.springframework.data.neo4j.fieldaccess; import org.springframework.beans.factory.annotation.Configurable; import org.springframework.core.convert.ConversionService; - import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty; import org.springframework.data.neo4j.support.Neo4jTemplate; @@ -44,7 +43,7 @@ public class ConvertingNodePropertyFieldAccessorFactory implements FieldAccessor @Override public boolean accept(final Neo4jPersistentProperty property) { final ConversionService conversionService = getConversionService(); - return property.isSerializableField(conversionService) && property.isDeserializableField(conversionService); + return property.isSerializablePropertyField(conversionService); } @Override @@ -54,28 +53,31 @@ public class ConvertingNodePropertyFieldAccessorFactory implements FieldAccessor public static class ConvertingNodePropertyFieldAccessor extends PropertyFieldAccessorFactory.PropertyFieldAccessor { + private final Class targetType = String.class; + private final PropertyConverter propertyConverter; + public ConvertingNodePropertyFieldAccessor(Neo4jPersistentProperty property, Neo4jTemplate template) { super(template, property); + propertyConverter = new PropertyConverter(template.getConversionService(),property); } @Override public Object setValue(final Object entity, final Object newVal) { - super.setValue(entity, serializePropertyValue(newVal)); + super.setValue(entity, propertyConverter.serializePropertyValue(newVal,targetType)); return newVal; } @Override public Object doGetValue(final Object entity) { - return deserializePropertyValue(super.doGetValue(entity)); + return propertyConverter.deserializePropertyValue(super.doGetValue(entity)); } - private Object serializePropertyValue(final Object newVal) { - return template.getConversionService().convert(newVal, String.class); + @Override + protected Object convertSimplePropertyValue(Object value) { + return value; } - private Object deserializePropertyValue(final Object value) { - return template.getConversionService().convert(value, fieldType); - } } + } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/PropertyConverter.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/PropertyConverter.java new file mode 100644 index 000000000..c4086b56f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/PropertyConverter.java @@ -0,0 +1,95 @@ +/** + * 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.core.convert.ConversionService; +import org.springframework.data.neo4j.mapping.Neo4jPersistentProperty; +import org.springframework.data.util.TypeInformation; + +import java.lang.reflect.Array; +import java.util.*; + +import static java.util.Arrays.asList; + +/** + * @author mh + * @since 05.11.11 + */ +public class PropertyConverter { + private final ConversionService conversionService; + private final Neo4jPersistentProperty property; + private final TypeInformation typeInformation; + + public PropertyConverter(ConversionService conversionService, Neo4jPersistentProperty property) { + this.conversionService = conversionService; + this.property = property; + this.typeInformation = property.getTypeInformation(); + } + + public Object serializePropertyValue(final Object newVal, Class targetType) { + if (newVal == null) return null; + final TypeInformation typeInformation = property.getTypeInformation(); + if (typeInformation.isCollectionLike()) { + return serializeCollection(newVal, conversionService, typeInformation, targetType); + } + return conversionService.convert(newVal, targetType); + } + + public Object deserializePropertyValue(final Object newVal) { + if (newVal == null) return null; + if (typeInformation.isCollectionLike() && isCollectionLike(newVal)) { + return deserializeCollection(newVal, conversionService, typeInformation); + } + return conversionService.convert(newVal, typeInformation.getType()); + } + + private boolean isCollectionLike(Object val) { + return val != null && (val.getClass().isArray() || Collection.class.isAssignableFrom(val.getClass())); + } + + private Object serializeCollection(Object newVal, ConversionService conversionService, TypeInformation typeInformation, final Class targetType) { + final List values = convertCollection(conversionService, targetType, toCollection(newVal)); + return values.toArray((Object[]) Array.newInstance(targetType, values.size())); + } + + @SuppressWarnings("unchecked") + private Object deserializeCollection(Object newVal, ConversionService conversionService, TypeInformation typeInformation) { + final Class actualType = typeInformation.getActualType().getType(); + final List result = convertCollection(conversionService, actualType, toCollection(newVal)); + final Class fieldType = typeInformation.getType(); + if (fieldType.isArray()) { + return result.toArray((Object[]) Array.newInstance(actualType, result.size())); + } + if (Set.class.isAssignableFrom(fieldType)) return new LinkedHashSet(result); + return result; + } + + private List convertCollection(ConversionService conversionService, Class targetType, Iterable values) { + final List result = new ArrayList(); + for (Object value : values) { + result.add(conversionService.convert(value, targetType)); + } + return result; + } + + private Iterable toCollection(Object newVal) { + if (newVal.getClass().isArray()) { + return asList((Object[]) newVal); + } else { + return (Iterable) newVal; + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/PropertyFieldAccessorFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/PropertyFieldAccessorFactory.java index 39c045239..913048fd0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/PropertyFieldAccessorFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/PropertyFieldAccessorFactory.java @@ -84,14 +84,18 @@ public class PropertyFieldAccessorFactory implements FieldAccessorFactory { if (element.hasProperty(propertyName)) { Object value = element.getProperty(propertyName); if (value == null || fieldType.isInstance(value)) return value; - if (template.getConversionService() !=null) { - return template.getConversionService().convert(value, fieldType); - } - return value; + return convertSimplePropertyValue(value); } return getDefaultValue(fieldType); } + protected Object convertSimplePropertyValue(Object value) { + if (template.getConversionService() !=null) { + return template.getConversionService().convert(value, fieldType); + } + return value; + } + private Object getDefaultValue(final Class type) { if (type.isPrimitive()) { if (type.equals(boolean.class)) return false; 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 5d9dd4c09..518cdfcba 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 @@ -52,11 +52,7 @@ public interface Neo4jPersistentProperty extends PersistentProperty type = getType(); + final Class targetType = String.class; + if (getTypeInformation().isCollectionLike()) return isConvertibleBetween(conversionService, getComponentType(), targetType); + return isConvertibleBetween(conversionService, type, targetType); } - public boolean isSerializableField(final ConversionService conversionService) { - return isSimpleValueField() && conversionService.canConvert(getType(), String.class); - } - - public boolean isDeserializableField(final ConversionService conversionService) { - return isSimpleValueField() && conversionService.canConvert(String.class, getType()); + private boolean isConvertibleBetween(ConversionService conversionService, Class type, Class targetType) { + return conversionService.canConvert(type, targetType) && conversionService.canConvert(targetType, type); } @Override