From 604966838af7ba6b3ce7bc447b1e9eac8155a97c Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Thu, 24 Aug 2023 12:39:52 +0200 Subject: [PATCH] Add dynamic properties support for collections. --- .../typeconversion/MapCompositeConverter.java | 29 +++++++++++++-- .../org/neo4j/ogm/domain/properties/User.java | 25 +++++++++++++ .../types/properties/PropertiesTest.java | 37 +++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/neo4j/ogm/typeconversion/MapCompositeConverter.java b/core/src/main/java/org/neo4j/ogm/typeconversion/MapCompositeConverter.java index 1d56c2e66..ba4848795 100644 --- a/core/src/main/java/org/neo4j/ogm/typeconversion/MapCompositeConverter.java +++ b/core/src/main/java/org/neo4j/ogm/typeconversion/MapCompositeConverter.java @@ -27,10 +27,15 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; import java.util.function.Predicate; @@ -123,6 +128,8 @@ private void addMapToProperties(Map fieldValue, Map graphP @SuppressWarnings("unchecked") // It is checked by the condition above. EnumStringConverter enumStringConverter = converterCache.computeIfAbsent((Class>) entryValue.getClass(), EnumStringConverter::new); graphProperties.put(entryKey, enumStringConverter.toGraphProperty((Enum) entryValue)); + } else if (Set.class.isAssignableFrom(entryValue.getClass())) { + graphProperties.put(entryKey, new ArrayList<>((Set) entryValue)); // todo maps more than required } else { throw new MappingException("Could not map key=" + entryPrefix + entry.getKey() + ", " + "value=" + entryValue + " (type = " + entryValue.getClass() + ") " + @@ -166,16 +173,24 @@ private void putToMap(Map result, String propertyKey, Object val o = new HashMap<>(); result.put(keyInstance, o); } - putToMap(o, propertyKey.substring(delimiterIndex + delimiter.length()), value, nestedFieldType(fieldType)); + putToMap(o, propertyKey.substring(delimiterIndex + delimiter.length()), value, nestedMapFieldType(fieldType)); } else { Object keyInstance = keyInstanceFromString(propertyKey, getKeyType(fieldType)); - Type valueType = nestedFieldType(fieldType); + Type valueType = nestedMapFieldType(fieldType); if (valueType != null) { if (value instanceof String && ClassUtils.isEnum(valueType)) { @SuppressWarnings("unchecked") EnumStringConverter enumStringConverter = converterCache.computeIfAbsent((Class>) valueType, EnumStringConverter::new); value = enumStringConverter.toEntityAttribute((String) value); + } else if (valueType instanceof ParameterizedType) { + value = Utils.coerceTypes((Class) nestedCollectionFieldType(valueType), value); + Class rawCollectionType = (Class) ((ParameterizedType) valueType).getRawType(); + if (SortedSet.class.isAssignableFrom(rawCollectionType)) { + value = new TreeSet<>((Collection) value); + } else if (Set.class.isAssignableFrom(rawCollectionType)) { + value = new HashSet<>((Collection) value); + } } else { value = Utils.coerceTypes((Class) valueType, value); } @@ -193,7 +208,7 @@ private Class getKeyType(Type fieldType) { } } - private Type nestedFieldType(Type keyType) { + private Type nestedMapFieldType(Type keyType) { if (keyType instanceof ParameterizedType) { return ((ParameterizedType) keyType).getActualTypeArguments()[1]; } else { @@ -201,6 +216,14 @@ private Type nestedFieldType(Type keyType) { } } + private Type nestedCollectionFieldType(Type keyType) { + if (keyType instanceof ParameterizedType) { + return ((ParameterizedType) keyType).getActualTypeArguments()[0]; + } else { + return null; + } + } + private String keyInstanceToString(Object propertyKey) { if (propertyKey == null) { throw new UnsupportedOperationException("Null is not a supported property key!"); diff --git a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/properties/User.java b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/properties/User.java index 17cd99ca5..ed6a8a8da 100644 --- a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/properties/User.java +++ b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/properties/User.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; @@ -44,6 +45,7 @@ @NodeEntity public class User { + public enum EnumA {VALUE_AA} public enum EnumB { @@ -89,12 +91,19 @@ public String toString() { @Properties(transformEnumKeysWith = LowerCasePropertiesFilter.class) private Map filteredProperties; + @Properties(prefix = "listProperties", allowCast = true) + private Map> listProperties; + + @Properties(prefix = "setProperties", allowCast = true) + private Map> setProperties; + @Convert(NoPrefixPropertiesConverter.class) private Map manualProperties; @Relationship(type = "VISITED") private Set visits; + public User() { } @@ -258,6 +267,22 @@ public void setEnumBValuesByEnum( this.enumBValuesByEnum = enumBValuesByEnum; } + public void setListProperties(Map> listProperties) { + this.listProperties = listProperties; + } + + public Map> getListProperties() { + return listProperties; + } + + public Map> getSetProperties() { + return setProperties; + } + + public void setSetProperties(Map> setProperties) { + this.setProperties = setProperties; + } + public static class LowerCasePropertiesFilter implements BiFunction { @Override diff --git a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/persistence/types/properties/PropertiesTest.java b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/persistence/types/properties/PropertiesTest.java index 805584b05..a4f469b8b 100644 --- a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/persistence/types/properties/PropertiesTest.java +++ b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/persistence/types/properties/PropertiesTest.java @@ -25,8 +25,11 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Map; +import java.util.Set; import org.junit.Before; import org.junit.BeforeClass; @@ -452,4 +455,38 @@ public void manualConversionShouldSupportPropertiesWithouthPrefix() { assertThat(user.getMyProperties()) .containsOnlyKeys("prop1", "prop2"); } + + @Test //GH-955 + public void shouldHandleListOfProperties() { + User user = new User(); + Map> properties = new HashMap<>(); + properties.put("a", Arrays.asList("a", "b")); + properties.put("b", Arrays.asList("c", "d")); + user.setListProperties(properties); + + session.save(user); + session.clear(); + + user = session.load(User.class, user.getId()); + assertThat(user.getListProperties()) + .containsOnly(new HashMap.SimpleEntry<>("a", Arrays.asList("a", "b")), new HashMap.SimpleEntry<>("b", Arrays.asList("c", "d"))); + } + + @Test //GH-955 + public void shouldHandleSetOfProperties() { + User user = new User(); + Map> properties = new HashMap<>(); + Set a = new HashSet<>(Arrays.asList("a", "b")); + Set b = new HashSet<>(Arrays.asList("c", "d")); + properties.put("a", a); + properties.put("b", b); + user.setSetProperties(properties); + + session.save(user); + session.clear(); + + user = session.load(User.class, user.getId()); + assertThat(user.getSetProperties()) + .containsOnly(new HashMap.SimpleEntry<>("a", new HashSet<>(Arrays.asList("a", "b"))), new HashMap.SimpleEntry<>("b", new HashSet<>(Arrays.asList("c", "d")))); + } }