diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/DynamicProperties.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/DynamicProperties.java new file mode 100644 index 00000000..fd018a62 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/DynamicProperties.java @@ -0,0 +1,124 @@ +/** + * 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 java.lang.reflect.Field; +import java.util.Map; + +import org.springframework.data.neo4j.fieldaccess.DynamicPropertiesFieldAccessorFactory.DynamicPropertiesFieldAccessor; + +/** + * A {@link DynamicProperties} property on a @NodeEntity stores all its properties dynamically + * on the underlying node itself. + *

+ * This dynamic property only is available inside a transaction, i.e. when the entity has been saved. + *

+ * The key/value pairs of the {@link DynamicProperties} property are stored on the node with the keys + * prefixed with the property name that is returned by {@link DelegatingFieldAccessorFactory#getNeo4jPropertyName(Field)}. + *

+ * @NodeEntity
+ * class Person {
+ *     String name;
+ *     DynamicProperties personalProperties;
+ * }
+ * 
+ * Person p = new Person();
+ * p.persist();
+ * p.personalProperties.setProperty("ZIP", 8000);
+ * p.personalProperties.setProperty("City", "Zürich");
+ * 
+ * results in a node with the properties: + *
+ * "personalProperties-ZIP" => 8000
+ * "personalProperties-City" => "Zürich"
+ * 
+ */ +public interface DynamicProperties { + + /** + * @param key + * the key to be checked + * @return true if a property with the given key exists + */ + boolean hasProperty(String key); + + /** + * @param key + * key of the property to get + * @return the property with the given key, or null if no such property exists and {@link #hasProperty} + * returns false + */ + Object getProperty(String key); + + /** + * @param key + * key of the property to get + * @param defaultValue + * the default value to return if no property with the given key exists + * @return the property with the given key or defaultValue if no such property exists and {@link #hasProperty} + * returns false + */ + Object getProperty(String key, Object defaultValue); + + /** + * Set the value of the property with the given key to the given value and overwrites it when such a property + * already exists. + * + * @param key + * key of the property + * @param value + * value of the property + */ + void setProperty(String key, Object value); + + /** + * Removes the property with the given key + * + * @param key + * @return the property that has been removed or null if no such property exists and {@link #hasProperty} returns + * false + */ + Object removeProperty(String key); + + /** + * Returns all keys + * + * @return iterable over all keys + */ + Iterable getPropertyKeys(); + + /** + * @return a map with all properties key/value pairs + */ + Map asMap(); + + /** + * Sets a property for all key/value pairs in the given map + * + * @param map + * that contains the key/value pairs to set + */ + void setPropertiesFrom(Map map); + + /** + * Creates a new instance with the properties set from the given map with {@link #setPropertiesFrom(Map)} + * + * @param map + * that contains the key/value pairs to set + * @return a new DynamicProperties instance + */ + DynamicProperties createFrom(Map map); +} \ No newline at end of file diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/DynamicPropertiesFieldAccessorFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/DynamicPropertiesFieldAccessorFactory.java new file mode 100644 index 00000000..9cb0f005 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/DynamicPropertiesFieldAccessorFactory.java @@ -0,0 +1,110 @@ +/** + * 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 java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.apache.commons.collections.CollectionUtils; +import org.neo4j.graphdb.PropertyContainer; +import org.neo4j.helpers.collection.IteratorUtil; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.neo4j.core.GraphBacked; +import org.springframework.data.neo4j.support.DoReturn; + +/** + * This accessor factory creates {@link DynamicPropertiesFieldAccessor}s for @NodeEntity properties of type + * {@link DynamicProperties}. + */ +public class DynamicPropertiesFieldAccessorFactory implements FieldAccessorFactory> { + + private final ConversionService conversionService; + + public DynamicPropertiesFieldAccessorFactory(final ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Override + public boolean accept(Field f) { + return DynamicProperties.class.isAssignableFrom(f.getType()); + } + + @Override + public FieldAccessor> forField(Field field) { + return new DynamicPropertiesFieldAccessor(conversionService, + DelegatingFieldAccessorFactory.getNeo4jPropertyName(field), field); + } + + public static class DynamicPropertiesFieldAccessor implements FieldAccessor> { + private final ConversionService conversionService; + private final String propertyNamePrefix; + private final Field field; + + public DynamicPropertiesFieldAccessor(ConversionService conversionService, String propertyName, Field field) { + this.conversionService = conversionService; + this.propertyNamePrefix = propertyName; + this.field = field; + } + + @Override + public Object setValue(final GraphBacked entity, final Object newVal) { + final PropertyContainer propertyContainer = entity.getPersistentState(); + ManagedPrefixedDynamicProperties dynamicProperties = (ManagedPrefixedDynamicProperties) newVal; + + Set dynamicProps = dynamicProperties.getPrefixedPropertyKeys(); + Set nodeProps = new HashSet(); + IteratorUtil.addToCollection(propertyContainer.getPropertyKeys(), nodeProps); + + // Get the properties that are not present in the DynamicProperties container anymore + // by removing all present keys from the actual node properties. + for (String prop : dynamicProps) { + nodeProps.remove(prop); + } + + // nodeProps now contains the properties that are present on the node, but not in the DynamicProperties - + // in other words: properties that have been removed. Remove them from the node as well. + for(String removedKey : nodeProps) { + propertyContainer.removeProperty(removedKey); + } + + // Add all properties to the propertyContainer + for (String key : dynamicProps) { + propertyContainer.setProperty(key, dynamicProperties.getPrefixedProperty(key)); + } + return newVal; + } + + @Override + public Object getValue(final GraphBacked entity) { + PropertyContainer element = entity.getPersistentState(); + ManagedPrefixedDynamicProperties props = ManagedPrefixedDynamicProperties.create(propertyNamePrefix, + field, entity); + for (String key : element.getPropertyKeys()) { + props.setPropertyIfPrefixed(key, element.getProperty(key)); + } + return DoReturn.doReturn(props); + } + + @Override + public boolean isWriteable(final GraphBacked entity) { + return true; + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/ManagedPrefixedDynamicProperties.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/ManagedPrefixedDynamicProperties.java new file mode 100644 index 00000000..fbee26c1 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/ManagedPrefixedDynamicProperties.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.fieldaccess; + +import java.lang.reflect.Field; +import java.util.Map; + +import org.neo4j.graphdb.Node; +import org.springframework.data.neo4j.core.EntityState; +import org.springframework.data.neo4j.core.NodeBacked; +import org.springframework.data.neo4j.core.RelationshipBacked; +import org.springframework.data.neo4j.support.DoReturn; + +/** + * Updates the entity containing such a ManagedPrefixedDynamicProperties when some property is added, changed or + * deleted. + * + * @param + * type of the entity (Node or Relationships) + */ +public class ManagedPrefixedDynamicProperties extends PrefixedDynamicProperties { + private final ENTITY entity; + private final Field field; + + public ManagedPrefixedDynamicProperties(String prefix, final Field field, final ENTITY entity) { + super(prefix); + this.field = field; + this.entity = entity; + } + + public ManagedPrefixedDynamicProperties(String prefix, int initialCapacity, final Field field, final ENTITY entity) { + super(prefix, initialCapacity); + this.field = field; + this.entity = entity; + } + + public static ManagedPrefixedDynamicProperties create(String prefix, final Field field, + final E entity) { + return new ManagedPrefixedDynamicProperties(prefix, field, entity); + } + + @Override + public void setProperty(String key, Object value) { + super.setProperty(key, value); + update(); + } + + @Override + public Object removeProperty(String key) { + Object o = super.removeProperty(key); + update(); + return o; + } + + @Override + public void setPropertiesFrom(Map map) { + super.setPropertiesFrom(map); + update(); + } + + @Override + public DynamicProperties createFrom(Map map) { + DynamicProperties d = new ManagedPrefixedDynamicProperties(prefix, map.size(), field, entity); + d.setPropertiesFrom(map); + update(); + return d; + } + + private void update() { + if (entity instanceof NodeBacked) { + NodeBacked nodeBacked = (NodeBacked) entity; + final EntityState entityState = nodeBacked.getEntityState(); + updateValue(entityState); + } + if (entity instanceof RelationshipBacked) { + RelationshipBacked relationshipBacked = (RelationshipBacked) entity; + updateValue(relationshipBacked.getEntityState()); + } + } + + private Object updateValue(EntityState entityState) { + try { + final Object newValue = entityState.setValue(field, this); + if (newValue instanceof DoReturn) + return DoReturn.unwrap(newValue); + field.setAccessible(true); + field.set(entity, newValue); + return newValue; + } catch (IllegalAccessException e) { + throw new RuntimeException("Could not update field " + field + " to new value of type " + + this.getClass()); + } + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/NodeDelegatingFieldAccessorFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/NodeDelegatingFieldAccessorFactory.java index 588566f1..e877f96b 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/NodeDelegatingFieldAccessorFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/NodeDelegatingFieldAccessorFactory.java @@ -55,7 +55,8 @@ protected Collection> createAccessorFactories( new SingleRelationshipFieldAccessorFactory(graphDatabaseContext), new OneToNRelationshipFieldAccessorFactory(graphDatabaseContext), new ReadOnlyOneToNRelationshipFieldAccessorFactory(graphDatabaseContext), - new OneToNRelationshipEntityFieldAccessorFactory(graphDatabaseContext) + new OneToNRelationshipEntityFieldAccessorFactory(graphDatabaseContext), + new DynamicPropertiesFieldAccessorFactory(graphDatabaseContext.getConversionService()) ); } } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/PrefixedDynamicProperties.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/PrefixedDynamicProperties.java new file mode 100644 index 00000000..9c1daf9f --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/PrefixedDynamicProperties.java @@ -0,0 +1,244 @@ +/** + * 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 java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * Stores the properties internally with prefixed keys. When using the methods from {@link DynamicProperties} the prefix + * is dynamically added and removed so that the prefixing is not visible when using the {@link DynamicProperties} + * interface. + *

+ * The methods *PrefixedProperty() allow to access the prefixed property key/values pairs directly. + */ +public class PrefixedDynamicProperties implements DynamicProperties { + private final Map map; + protected final String prefix; + + /** + * Handles key prefixing + */ + private static class PrefixUtil { + private final String prefix; + private final static String DIVIDER = "-"; + + public PrefixUtil(final String prefix) { + this.prefix = prefix + DIVIDER; + } + + boolean hasPrefix(final String s) { + return s.startsWith(prefix); + } + + public String removePrefix(final String s) { + if (hasPrefix(s)) { + return s.substring(prefix.length()); + } + else { + return s; + } + } + + public static String prefixKey(final String prefix, final String key) { + return new StringBuilder(prefix).append(DIVIDER).append(key).toString(); + } + } + + /** + * Removes a prefix from Strings when iterating over them. + */ + private static class RemovePrefixIterableWrapper implements Iterable { + + private final Iterable iterable; + private final String prefix; + + /** + * + * @param iterable + * Strings to iterate over + * @param prefix + * the prefix to have removed from the iterated Strings + */ + RemovePrefixIterableWrapper(final Iterable iterable, final String prefix) { + this.iterable = iterable; + this.prefix = prefix; + } + + @Override + public Iterator iterator() { + return new RemovePrefixIteratorWrapper(iterable.iterator(), prefix); + } + + /** + * Removes a prefix from Strings when iterating over them. + */ + private static class RemovePrefixIteratorWrapper implements Iterator { + + private final Iterator it; + private final PrefixUtil prefixUtil; + + /** + * + * @param it + * Strings to iterate over + * @param prefix + * the prefix to have removed from the iterated Strings + */ + private RemovePrefixIteratorWrapper(final Iterator it, final String prefix) { + this.prefixUtil = new PrefixUtil(prefix); + this.it = it; + } + + @Override + public boolean hasNext() { + return it.hasNext(); + } + + /** + * Returns the next string with the prefix removed + */ + @Override + public String next() { + return prefixUtil.removePrefix(it.next()); + } + + @Override + public void remove() { + it.remove(); + } + + } + + } + + /** + * @param prefix + * the prefix to be added internally to the keys + */ + public PrefixedDynamicProperties(final String prefix) { + map = new HashMap(); + this.prefix = prefix; + } + + /** + * @param prefix + * the prefix to be added internally to the keys + * @param initialCapacity + * the initialCapacity of the internal map that holds the properties + */ + public PrefixedDynamicProperties(final String prefix, final int initialCapacity) { + map = new HashMap(initialCapacity); + this.prefix = prefix; + } + + @Override + public boolean hasProperty(final String key) { + return map.containsKey(prefixedKey(key)); + } + + @Override + public Object getProperty(final String key) { + return map.get(prefixedKey(key)); + } + + @Override + public Object getProperty(final String key, final Object defaultValue) { + if (!hasProperty(key)) { + return defaultValue; + } + return getProperty(key); + } + + @Override + public void setProperty(final String key, final Object value) { + map.put(prefixedKey(key), value); + } + + @Override + public Object removeProperty(final String key) { + return map.remove(prefixedKey(key)); + } + + @Override + public Iterable getPropertyKeys() { + return new RemovePrefixIterableWrapper(map.keySet(), prefix); + } + + @Override + public void setPropertiesFrom(final Map map) { + for (String key : map.keySet()) { + setProperty(key, map.get(key)); + } + } + + @Override + public DynamicProperties createFrom(final Map map) { + DynamicProperties d = new PrefixedDynamicProperties(prefix, map.size()); + d.setPropertiesFrom(map); + return d; + } + + @Override + public Map asMap() { + Map result = new HashMap(map.size()); + for (String key : getPropertyKeys()) { + result.put(key, getProperty(key)); + } + return result; + } + + /** + * Set the property with the given key only if the key is prefixed. + * + * @param key + * key of the property + * @param value + * value + * @return true if the property has been set or not + */ + public boolean setPropertyIfPrefixed(final String key, final Object value) { + PrefixUtil util = new PrefixUtil(prefix); + if (util.hasPrefix(key)) { + setPrefixedProperty(key, value); + return true; + } + return false; + } + + private String prefixedKey(final String key) { + return PrefixUtil.prefixKey(prefix, key); + } + + public Object getPrefixedProperty(final String key) { + return map.get(key); + } + + public void setPrefixedProperty(final String key, final Object value) { + map.put(key, value); + } + + public boolean hasPrefixedProperty(final String key) { + return map.containsKey(key); + } + + public Set getPrefixedPropertyKeys() { + return map.keySet(); + } + +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelationshipDelegatingFieldAccessorFactory.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelationshipDelegatingFieldAccessorFactory.java index 6a9d2f08..c8a7be12 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelationshipDelegatingFieldAccessorFactory.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/fieldaccess/RelationshipDelegatingFieldAccessorFactory.java @@ -43,7 +43,8 @@ protected Collection> createAccessorFactories( new TransientFieldAccessorFactory(), new RelationshipNodeFieldAccessorFactory(graphDatabaseContext), new PropertyFieldAccessorFactory(graphDatabaseContext.getConversionService()), - new ConvertingNodePropertyFieldAccessorFactory(graphDatabaseContext.getConversionService()) + new ConvertingNodePropertyFieldAccessorFactory(graphDatabaseContext.getConversionService()), + new DynamicPropertiesFieldAccessorFactory(graphDatabaseContext.getConversionService()) ); } } \ No newline at end of file diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/Friendship.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/Friendship.java index f75e3c00..c77f3bd1 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/Friendship.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/Friendship.java @@ -17,12 +17,13 @@ package org.springframework.data.neo4j; +import java.util.Date; + import org.springframework.data.neo4j.annotation.EndNode; import org.springframework.data.neo4j.annotation.Indexed; import org.springframework.data.neo4j.annotation.RelationshipEntity; import org.springframework.data.neo4j.annotation.StartNode; - -import java.util.Date; +import org.springframework.data.neo4j.fieldaccess.DynamicProperties; @RelationshipEntity(useShortNames = false) public class Friendship { @@ -52,6 +53,8 @@ public Friendship(Person p1, Person p2, int years) { private Date firstMeetingDate; + private DynamicProperties personalProperties; + private transient String latestLocation; public void setPerson1(Person p) { @@ -93,4 +96,12 @@ public void setLatestLocation(String latestLocation) { public String getLatestLocation() { return latestLocation; } + + public void setPersonalProperties(DynamicProperties personalProperties) { + this.personalProperties = personalProperties; + } + + public DynamicProperties getPersonalProperties() { + return personalProperties; + } } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/Person.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/Person.java index c5c4ec44..f830afae 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/Person.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/Person.java @@ -16,14 +16,20 @@ package org.springframework.data.neo4j; -import org.springframework.data.neo4j.annotation.*; -import org.springframework.data.neo4j.core.Direction; +import java.util.Date; +import java.util.Map; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.Size; -import java.util.Date; -import java.util.Map; + +import org.springframework.data.neo4j.annotation.Indexed; +import org.springframework.data.neo4j.annotation.NodeEntity; +import org.springframework.data.neo4j.annotation.Query; +import org.springframework.data.neo4j.annotation.RelatedTo; +import org.springframework.data.neo4j.annotation.RelatedToVia; +import org.springframework.data.neo4j.core.Direction; +import org.springframework.data.neo4j.fieldaccess.DynamicProperties; @NodeEntity @@ -57,6 +63,8 @@ public class Person { private Car car; + private DynamicProperties personalProperties; + @RelatedTo private Person mother; @@ -204,6 +212,22 @@ public Car getCar() { return car; } + public void setProperty(String key, Object value) { + personalProperties.setProperty(key, value); + } + + public Object getProperty(String key) { + return personalProperties.getProperty(key); + } + + public DynamicProperties getPersonalProperties() { + return personalProperties; + } + + public void setPersonalProperties(DynamicProperties personalProperties) { + this.personalProperties = personalProperties; + } + public void setNickname(String nickname) { this.nickname = nickname; } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/fieldaccess/PrefixedDynamicPropertyTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/fieldaccess/PrefixedDynamicPropertyTest.java new file mode 100644 index 00000000..80d11902 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/fieldaccess/PrefixedDynamicPropertyTest.java @@ -0,0 +1,117 @@ +/** + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.matchers.JUnitMatchers.hasItems; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +public class PrefixedDynamicPropertyTest { + private static final String VALUE = "value"; + private static final String VALUE2 = "value2"; + private static final String DEFAULT_VALUE = "default_value"; + private static final String KEY = "key"; + private static final String KEY2 = "key2"; + private static final String PREFIX = "foo"; + private static final String PREFIXED_KEY = PREFIX + "-" + KEY; + private static final String PREFIXED_KEY2 = PREFIX + "-" + KEY2; + PrefixedDynamicProperties props = new PrefixedDynamicProperties(PREFIX); + + private List toList(Iterable it) { + List result = new ArrayList(); + for (T t : it) { + result.add(t); + } + + return result; + } + + @Test + public void setProperty() { + assertFalse(props.hasProperty(KEY)); + assertFalse(props.hasPrefixedProperty(PREFIXED_KEY)); + assertEquals(props.getProperty(KEY, DEFAULT_VALUE), DEFAULT_VALUE); + + props.setProperty(KEY, VALUE); + + assertTrue(props.hasProperty(KEY)); + assertTrue(props.hasPrefixedProperty(PREFIXED_KEY)); + assertEquals(props.getProperty(KEY), VALUE); + assertEquals(props.getPrefixedProperty(PREFIXED_KEY), VALUE); + assertEquals(props.getProperty(KEY, DEFAULT_VALUE), VALUE); + + props.setProperty(KEY2, VALUE); + + assertThat(toList(props.getPropertyKeys()), hasItems(KEY, KEY2)); + assertThat(toList(props.getPrefixedPropertyKeys()), hasItems(PREFIXED_KEY, PREFIXED_KEY2)); + + props.removeProperty(KEY); + + assertFalse(props.hasProperty(KEY)); + assertFalse(props.hasPrefixedProperty(PREFIXED_KEY)); + } + + @Test + public void setPropertyIfPrefixed() { + props.setPropertyIfPrefixed(KEY, VALUE); + assertFalse(props.hasProperty(KEY)); + assertFalse(props.hasPrefixedProperty(PREFIXED_KEY)); + + props.setPropertyIfPrefixed(PREFIXED_KEY, VALUE); + assertTrue(props.hasProperty(KEY)); + assertTrue(props.hasPrefixedProperty(PREFIXED_KEY)); + } + + @Test + public void setRawProperty() { + props.setPrefixedProperty(PREFIXED_KEY, VALUE); + assertTrue(props.hasPrefixedProperty(PREFIXED_KEY)); + assertTrue(props.hasProperty(KEY)); + } + + @Test + public void asMap() { + props.setProperty(KEY, VALUE); + props.setProperty(KEY2, VALUE2); + + Map map = props.asMap(); + assertThat(map.keySet(), hasItems(KEY, KEY2)); + List values = new ArrayList(map.size()); + for (String key : map.keySet()) { + values.add((String) map.get(key)); + } + assertThat(values, hasItems(VALUE, VALUE2)); + } + + @Test + public void createFrom() { + Map m = new HashMap(); + m.put(KEY, VALUE); + m.put(KEY2, VALUE2); + DynamicProperties p = props.createFrom(m); + assertTrue(p.hasProperty(KEY)); + assertTrue(p.hasProperty(KEY2)); + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/DynamicPropertiesTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/DynamicPropertiesTest.java new file mode 100644 index 00000000..62c73931 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/support/DynamicPropertiesTest.java @@ -0,0 +1,177 @@ +/** + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.springframework.data.neo4j.Person.persistedPerson; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.neo4j.graphdb.Direction; +import org.neo4j.graphdb.DynamicRelationshipType; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.PropertyContainer; +import org.neo4j.graphdb.Relationship; +import org.neo4j.helpers.collection.IteratorUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.neo4j.Friendship; +import org.springframework.data.neo4j.Person; +import org.springframework.data.neo4j.fieldaccess.DynamicProperties; +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; + +@RunWith( SpringJUnit4ClassRunner.class ) +@ContextConfiguration( locations = {"classpath:org/springframework/data/neo4j/support/Neo4jGraphPersistenceTest-context.xml"} ) +public class DynamicPropertiesTest +{ + @Autowired + private GraphDatabaseContext graphDatabaseContext; + + @BeforeTransaction + public void cleanDb() { + Neo4jHelper.cleanDb(graphDatabaseContext); + } + + /** + * The dynamic properties can only be used, after the entity has been persisted and has an entity state. + */ + @Test(expected = java.lang.NullPointerException.class) + public void testCreateOutsideTransactionFails() { + Person p = new Person("James", 35); + p.setProperty("s", "String"); + } + + Person createTestPerson() { + Person p = persistedPerson("James", 36); + p.setProperty("s", "String"); + p.setProperty("x", 100); + p.setProperty("pi", 3.1415); + return p.persist(); + } + + @Test + @Transactional + public void testProperties() { + Person p = createTestPerson(); + assertEquals(3, IteratorUtil.count(p.getPersonalProperties().getPropertyKeys())); + assertProperties(nodeFor(p)); + } + + @Test + @Transactional + public void testRemoveProperty() { + Person p = createTestPerson(); + + DynamicProperties props = p.getPersonalProperties(); + props.removeProperty("s"); + p.persist(); + Node node = nodeFor(p); + assertEquals(2, IteratorUtil.count(p.getPersonalProperties().getPropertyKeys())); + assertFalse(node.hasProperty("personalProperties-s")); + assertEquals(100, node.getProperty("personalProperties-x")); + assertEquals(3.1415, ((Double)node.getProperty("personalProperties-pi")).doubleValue(), 0.000000001); + } + + @Test + @Transactional + public void testFromMap() { + Person p = persistedPerson("James", 36); + + Map propertyMap = new HashMap(); + propertyMap.put("s", "String"); + propertyMap.put("x", 100); + propertyMap.put("pi", 3.1415); + + p.setPersonalProperties(p.getPersonalProperties().createFrom(propertyMap)); + p.persist(); + assertEquals(3, IteratorUtil.count(p.getPersonalProperties().getPropertyKeys())); + assertProperties(nodeFor(p)); + } + + @Test + @Transactional + public void testAsMap() { + Person p = createTestPerson(); + Map propertyMap = p.getPersonalProperties().asMap(); + assertEquals(3, propertyMap.size()); + assertEquals(100, propertyMap.get("x")); + assertEquals(3.1415, ((Double)propertyMap.get("pi")).doubleValue(), 0.000000001); + assertEquals("String", propertyMap.get("s")); + } + + @Test + @Transactional + public void testRelationshipProperties() { + Person james = persistedPerson("James", 36); + Person john = persistedPerson("John", 36); + Friendship f = john.knows(james); + DynamicProperties props = f.getPersonalProperties(); + props.setProperty("s", "String"); + props.setProperty("x", 100); + props.setProperty("pi", 3.1415); + + Relationship rel = john.getPersistentState().getSingleRelationship(DynamicRelationshipType.withName("knows"), Direction.OUTGOING); + + assertProperties(rel, "Friendship."); + } + + @Test + @Transactional + public void testRelationshipRemoveProperty() { + Person james = persistedPerson("James", 36); + Person john = persistedPerson("John", 36); + Friendship f = john.knows(james); + DynamicProperties props = f.getPersonalProperties(); + props.setProperty("s", "String"); + props.setProperty("x", 100); + props.setProperty("pi", 3.1415); + + Relationship rel = john.getPersistentState().getSingleRelationship(DynamicRelationshipType.withName("knows"), Direction.OUTGOING); + assertProperties(rel, "Friendship."); + john.persist(); + + props.removeProperty("s"); + rel = john.getPersistentState().getSingleRelationship(DynamicRelationshipType.withName("knows"), Direction.OUTGOING); + + final String prefix = "Friendship."; + assertEquals(100, rel.getProperty(prefix + "personalProperties-x")); + assertEquals(3.1415, ((Double)rel.getProperty(prefix + "personalProperties-pi")).doubleValue(), 0.000000001); + assertFalse(rel.hasProperty(prefix + "personalProperties-s")); + } + + private static void assertProperties(PropertyContainer container) { + assertProperties(container, ""); + } + + private static void assertProperties(PropertyContainer container, String prefix) { + assertEquals(100, container.getProperty(prefix + "personalProperties-x")); + assertEquals(3.1415, ((Double)container.getProperty(prefix + "personalProperties-pi")).doubleValue(), 0.000000001); + assertEquals("String", container.getProperty(prefix + "personalProperties-s")); + } + + private Node nodeFor(Person person) { + return person.getPersistentState(); + } + +}