Skip to content

Commit

Permalink
DynamicProperties implementation as discussed in http://forum.springs…
Browse files Browse the repository at this point in the history
  • Loading branch information
ractive committed Sep 6, 2011
1 parent c06904a commit a757f98
Show file tree
Hide file tree
Showing 10 changed files with 924 additions and 8 deletions.
@@ -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.
* <p>
* This dynamic property only is available inside a transaction, i.e. when the entity has been saved.
* <p>
* 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)}.
* <pre>
* &#064;NodeEntity
* class Person {
* String name;
* DynamicProperties personalProperties;
* }
*
* Person p = new Person();
* p.persist();
* p.personalProperties.setProperty(&quot;ZIP&quot;, 8000);
* p.personalProperties.setProperty(&quot;City&quot;, &quot;Zürich&quot;);
* </pre>
* results in a node with the properties:
* <pre>
* "personalProperties-ZIP" => 8000
* "personalProperties-City" => "Zürich"
* </pre>
*/
public interface DynamicProperties {

/**
* @param key
* the key to be checked
* @return <tt>true</tt> 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 <tt>null</tt> if no such property exists and {@link #hasProperty}
* returns <tt>false</tt>
*/
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 <tt>false</tt>
*/
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
* <tt>false</tt>
*/
Object removeProperty(String key);

/**
* Returns all keys
*
* @return iterable over all keys
*/
Iterable<String> getPropertyKeys();

/**
* @return a map with all properties key/value pairs
*/
Map<String, Object> 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<String, Object> 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<String, Object> map);
}
@@ -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<GraphBacked<PropertyContainer>> {

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<GraphBacked<PropertyContainer>> forField(Field field) {
return new DynamicPropertiesFieldAccessor(conversionService,
DelegatingFieldAccessorFactory.getNeo4jPropertyName(field), field);
}

public static class DynamicPropertiesFieldAccessor implements FieldAccessor<GraphBacked<PropertyContainer>> {
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<PropertyContainer> entity, final Object newVal) {
final PropertyContainer propertyContainer = entity.getPersistentState();
ManagedPrefixedDynamicProperties<?> dynamicProperties = (ManagedPrefixedDynamicProperties<?>) newVal;

Set<String> dynamicProps = dynamicProperties.getPrefixedPropertyKeys();
Set<String> nodeProps = new HashSet<String>();
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<PropertyContainer> 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<PropertyContainer> entity) {
return true;
}
}
}
@@ -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 <ENTITY>
* type of the entity (Node or Relationships)
*/
public class ManagedPrefixedDynamicProperties<ENTITY> 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 <E> ManagedPrefixedDynamicProperties<E> create(String prefix, final Field field,
final E entity) {
return new ManagedPrefixedDynamicProperties<E>(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<String, Object> map) {
super.setPropertiesFrom(map);
update();
}

@Override
public DynamicProperties createFrom(Map<String, Object> map) {
DynamicProperties d = new ManagedPrefixedDynamicProperties<ENTITY>(prefix, map.size(), field, entity);
d.setPropertiesFrom(map);
update();
return d;
}

private void update() {
if (entity instanceof NodeBacked) {
NodeBacked nodeBacked = (NodeBacked) entity;
final EntityState<NodeBacked, Node> 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());
}
}
}
Expand Up @@ -55,7 +55,8 @@ protected Collection<? extends FieldAccessorFactory<?>> createAccessorFactories(
new SingleRelationshipFieldAccessorFactory(graphDatabaseContext),
new OneToNRelationshipFieldAccessorFactory(graphDatabaseContext),
new ReadOnlyOneToNRelationshipFieldAccessorFactory(graphDatabaseContext),
new OneToNRelationshipEntityFieldAccessorFactory(graphDatabaseContext)
new OneToNRelationshipEntityFieldAccessorFactory(graphDatabaseContext),
new DynamicPropertiesFieldAccessorFactory(graphDatabaseContext.getConversionService())
);
}
}

0 comments on commit a757f98

Please sign in to comment.