22
22
import java .util .Collection ;
23
23
import java .util .Collections ;
24
24
import java .util .HashMap ;
25
+ import java .util .HashSet ;
25
26
import java .util .Map ;
26
27
import java .util .Optional ;
28
+ import java .util .Set ;
29
+ import java .util .concurrent .atomic .AtomicInteger ;
27
30
import java .util .function .BiFunction ;
28
31
29
32
import org .neo4j .cypherdsl .core .Condition ;
33
36
import org .neo4j .cypherdsl .core .StatementBuilder ;
34
37
import org .springframework .data .domain .Example ;
35
38
import org .springframework .data .domain .ExampleMatcher ;
39
+ import org .springframework .data .mapping .PropertyPath ;
36
40
import org .springframework .data .neo4j .core .convert .Neo4jConversionService ;
37
41
import org .springframework .data .neo4j .core .mapping .Constants ;
38
42
import org .springframework .data .neo4j .core .mapping .GraphPropertyDescription ;
39
43
import org .springframework .data .neo4j .core .mapping .Neo4jMappingContext ;
40
44
import org .springframework .data .neo4j .core .mapping .Neo4jPersistentEntity ;
41
45
import org .springframework .data .neo4j .core .mapping .Neo4jPersistentProperty ;
42
46
import org .springframework .data .neo4j .core .mapping .NodeDescription ;
47
+ import org .springframework .data .neo4j .core .mapping .RelationshipDescription ;
43
48
import org .springframework .data .support .ExampleMatcherAccessor ;
44
49
import org .springframework .data .util .DirectFieldAccessFallbackBeanWrapper ;
50
+ import org .springframework .lang .Nullable ;
45
51
46
52
/**
47
53
* Support class for "query by example" executors.
@@ -56,80 +62,142 @@ final class Predicate {
56
62
57
63
static <S > Predicate create (Neo4jMappingContext mappingContext , Example <S > example ) {
58
64
59
- Neo4jPersistentEntity <?> probeNodeDescription = mappingContext .getRequiredPersistentEntity (example .getProbeType ());
65
+ Neo4jPersistentEntity <?> nodeDescription = mappingContext .getRequiredPersistentEntity (example .getProbeType ());
60
66
61
- Collection <GraphPropertyDescription > graphProperties = probeNodeDescription .getGraphProperties ();
67
+ Collection <GraphPropertyDescription > graphProperties = nodeDescription .getGraphProperties ();
62
68
DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper (example .getProbe ());
63
69
ExampleMatcher matcher = example .getMatcher ();
64
70
ExampleMatcher .MatchMode mode = matcher .getMatchMode ();
65
71
ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor (matcher );
72
+ AtomicInteger relationshipPatternCount = new AtomicInteger ();
66
73
67
- Predicate predicate = new Predicate (probeNodeDescription );
74
+ Predicate predicate = new Predicate (nodeDescription );
68
75
for (GraphPropertyDescription graphProperty : graphProperties ) {
76
+ PropertyPath propertyPath = PropertyPath .from (graphProperty .getFieldName (), nodeDescription .getTypeInformation ());
77
+ // create condition for every defined property
78
+ PropertyPathWrapper propertyPathWrapper = new PropertyPathWrapper (relationshipPatternCount .incrementAndGet (), mappingContext .getPersistentPropertyPath (propertyPath ), true );
79
+ addConditionAndParameters (mappingContext , nodeDescription , beanWrapper , mode , matcherAccessor , predicate , graphProperty , propertyPathWrapper );
80
+ }
81
+
82
+ processRelationships (mappingContext , example , nodeDescription , beanWrapper , mode , relationshipPatternCount , null , predicate );
83
+
84
+ return predicate ;
85
+ }
69
86
70
- // TODO Relationships are not traversed.
87
+ private static <S > void processRelationships (Neo4jMappingContext mappingContext , Example <S > example , NodeDescription <?> currentNodeDescription ,
88
+ DirectFieldAccessFallbackBeanWrapper beanWrapper , ExampleMatcher .MatchMode mode , AtomicInteger relationshipPatternCount ,
89
+ @ Nullable PropertyPath propertyPath , Predicate predicate ) {
71
90
72
- String currentPath = graphProperty .getFieldName ();
73
- if (matcherAccessor .isIgnoredPath (currentPath )) {
91
+ for (RelationshipDescription relationship : currentNodeDescription .getRelationships ()) {
92
+ String relationshipFieldName = relationship .getFieldName ();
93
+ Object relationshipObject = beanWrapper .getPropertyValue (relationshipFieldName );
94
+
95
+ if (relationshipObject == null ) {
74
96
continue ;
75
97
}
76
98
77
- boolean internalId = graphProperty .isIdProperty () && probeNodeDescription .isUsingInternalIds ();
78
- String propertyName = graphProperty .getPropertyName ();
99
+ // Right now we are only accepting the first element of a collection as a filter entry.
100
+ // Maybe combining multiple entities with AND might make sense.
101
+ if (relationshipObject instanceof Collection collection ) {
102
+ int collectionSize = collection .size ();
103
+ if (collectionSize > 1 ) {
104
+ throw new IllegalArgumentException ("Cannot have more than one related node per collection." );
105
+ }
106
+ if (collectionSize == 0 ) {
107
+ continue ;
108
+ }
109
+ relationshipObject = collection .iterator ().next ();
79
110
80
- ExampleMatcher .PropertyValueTransformer transformer = matcherAccessor
81
- .getValueTransformerForPath (currentPath );
82
- Optional <Object > optionalValue = transformer
83
- .apply (Optional .ofNullable (beanWrapper .getPropertyValue (currentPath )));
111
+ }
112
+ NodeDescription <?> relatedNodeDescription = mappingContext .getNodeDescription (relationshipObject .getClass ());
84
113
85
- if (optionalValue .isEmpty ()) {
86
- if (!internalId && matcherAccessor .getNullHandler ().equals (ExampleMatcher .NullHandler .INCLUDE )) {
87
- predicate .add (mode , property (Constants .NAME_OF_TYPED_ROOT_NODE .apply (probeNodeDescription ), propertyName ).isNull ());
88
- }
89
- continue ;
114
+ // if we come from the root object, the path is probably _null_,
115
+ // and it needs to get initialized with the property name of the relationship
116
+ PropertyPath nestedPropertyPath = propertyPath == null
117
+ ? PropertyPath .from (relationshipFieldName , currentNodeDescription .getUnderlyingClass ())
118
+ : propertyPath .nested (relationshipFieldName );
119
+
120
+ PropertyPathWrapper nestedPropertyPathWrapper = new PropertyPathWrapper (relationshipPatternCount .incrementAndGet (), mappingContext .getPersistentPropertyPath (nestedPropertyPath ), false );
121
+ predicate .addRelationship (nestedPropertyPathWrapper );
122
+
123
+ for (GraphPropertyDescription graphProperty : relatedNodeDescription .getGraphProperties ()) {
124
+ addConditionAndParameters (mappingContext , (Neo4jPersistentEntity <?>) relatedNodeDescription , new DirectFieldAccessFallbackBeanWrapper (relationshipObject ), mode ,
125
+ new ExampleMatcherAccessor (example .getMatcher ()), predicate ,
126
+ graphProperty , nestedPropertyPathWrapper );
90
127
}
91
128
92
- Neo4jConversionService conversionService = mappingContext .getConversionService ();
129
+ processRelationships (mappingContext , example , relatedNodeDescription , new DirectFieldAccessFallbackBeanWrapper (relationshipObject ), mode , relationshipPatternCount ,
130
+ nestedPropertyPath , predicate );
131
+
132
+ }
133
+ }
134
+
135
+ private static void addConditionAndParameters (Neo4jMappingContext mappingContext , Neo4jPersistentEntity <?> nodeDescription , DirectFieldAccessFallbackBeanWrapper beanWrapper ,
136
+ ExampleMatcher .MatchMode mode , ExampleMatcherAccessor matcherAccessor , Predicate predicate , GraphPropertyDescription graphProperty ,
137
+ PropertyPathWrapper wrapper ) {
138
+
139
+ String currentPath = graphProperty .getFieldName ();
140
+ if (matcherAccessor .isIgnoredPath (currentPath )) {
141
+ return ;
142
+ }
93
143
94
- if (graphProperty .isRelationship ()) {
95
- Neo4jQuerySupport .REPOSITORY_QUERY_LOG .error ("Querying by example does not support traversing of relationships." );
96
- } else if (graphProperty .isIdProperty () && probeNodeDescription .isUsingInternalIds ()) {
144
+ boolean internalId = graphProperty .isIdProperty () && nodeDescription .isUsingInternalIds ();
145
+ String propertyName = graphProperty .getPropertyName ();
146
+
147
+ ExampleMatcher .PropertyValueTransformer transformer = matcherAccessor
148
+ .getValueTransformerForPath (currentPath );
149
+ Optional <Object > optionalValue = transformer
150
+ .apply (Optional .ofNullable (beanWrapper .getPropertyValue (currentPath )));
151
+
152
+ if (optionalValue .isEmpty ()) {
153
+ if (!internalId && matcherAccessor .getNullHandler ().equals (ExampleMatcher .NullHandler .INCLUDE )) {
154
+ predicate .add (mode , property (Constants .NAME_OF_TYPED_ROOT_NODE .apply (nodeDescription ), propertyName ).isNull ());
155
+ }
156
+ return ;
157
+ }
158
+
159
+ Neo4jConversionService conversionService = mappingContext .getConversionService ();
160
+ boolean isRootNode = predicate .neo4jPersistentEntity .equals (nodeDescription );
161
+
162
+ if (graphProperty .isIdProperty () && nodeDescription .isUsingInternalIds ()) {
163
+ if (isRootNode ) {
97
164
predicate .add (mode ,
98
165
predicate .neo4jPersistentEntity .getIdExpression ().isEqualTo (literalOf (optionalValue .get ())));
99
166
} else {
100
- Expression property = property (Constants .NAME_OF_TYPED_ROOT_NODE .apply (probeNodeDescription ), propertyName );
101
- Expression parameter = parameter (propertyName );
102
- Condition condition = property .isEqualTo (parameter );
103
-
104
- if (String .class .equals (graphProperty .getActualType ())) {
105
-
106
- if (matcherAccessor .isIgnoreCaseForPath (currentPath )) {
107
- property = Functions .toLower (property );
108
- parameter = Functions .toLower (parameter );
109
- }
110
-
111
- condition = switch (matcherAccessor .getStringMatcherForPath (currentPath )) {
112
- case DEFAULT , EXACT ->
113
- // This needs to be recreated as both property and parameter might have changed above
114
- property .isEqualTo (parameter );
115
- case CONTAINING -> property .contains (parameter );
116
- case STARTING -> property .startsWith (parameter );
117
- case ENDING -> property .endsWith (parameter );
118
- case REGEX -> property .matches (parameter );
119
- };
167
+ predicate .add (mode ,
168
+ nodeDescription .getIdExpression ().isEqualTo (literalOf (optionalValue .get ())));
169
+ }
170
+ } else {
171
+ Expression property = !isRootNode ? property (wrapper .getNodeName (), propertyName ) : property (Constants .NAME_OF_TYPED_ROOT_NODE .apply (nodeDescription ), propertyName );
172
+ Expression parameter = parameter (wrapper .getNodeName () + propertyName );
173
+ Condition condition = property .isEqualTo (parameter );
174
+
175
+ if (String .class .equals (graphProperty .getActualType ())) {
176
+
177
+ if (matcherAccessor .isIgnoreCaseForPath (currentPath )) {
178
+ property = Functions .toLower (property );
179
+ parameter = Functions .toLower (parameter );
120
180
}
121
- predicate .add (mode , condition );
122
- predicate .parameters .put (propertyName , optionalValue .map (
123
- v -> {
124
- Neo4jPersistentProperty neo4jPersistentProperty = (Neo4jPersistentProperty ) graphProperty ;
125
- return conversionService .writeValue (v , neo4jPersistentProperty .getTypeInformation (),
126
- neo4jPersistentProperty .getOptionalConverter ());
127
- })
128
- .get ());
181
+
182
+ condition = switch (matcherAccessor .getStringMatcherForPath (currentPath )) {
183
+ case DEFAULT , EXACT ->
184
+ // This needs to be recreated as both property and parameter might have changed above
185
+ property .isEqualTo (parameter );
186
+ case CONTAINING -> property .contains (parameter );
187
+ case STARTING -> property .startsWith (parameter );
188
+ case ENDING -> property .endsWith (parameter );
189
+ case REGEX -> property .matches (parameter );
190
+ };
129
191
}
192
+ predicate .add (mode , condition );
193
+ predicate .parameters .put (wrapper .getNodeName () + propertyName , optionalValue .map (
194
+ v -> {
195
+ Neo4jPersistentProperty neo4jPersistentProperty = (Neo4jPersistentProperty ) graphProperty ;
196
+ return conversionService .writeValue (v , neo4jPersistentProperty .getTypeInformation (),
197
+ neo4jPersistentProperty .getOptionalConverter ());
198
+ })
199
+ .get ());
130
200
}
131
-
132
- return predicate ;
133
201
}
134
202
135
203
private final Neo4jPersistentEntity neo4jPersistentEntity ;
@@ -138,6 +206,8 @@ static <S> Predicate create(Neo4jMappingContext mappingContext, Example<S> examp
138
206
139
207
private final Map <String , Object > parameters = new HashMap <>();
140
208
209
+ private final Set <PropertyPathWrapper > relationshipFields = new HashSet <>();
210
+
141
211
private Predicate (Neo4jPersistentEntity neo4jPersistentEntity ) {
142
212
this .neo4jPersistentEntity = neo4jPersistentEntity ;
143
213
}
@@ -159,11 +229,19 @@ private void add(ExampleMatcher.MatchMode matchMode, Condition additionalConditi
159
229
};
160
230
}
161
231
232
+ private void addRelationship (PropertyPathWrapper propertyPathWrapper ) {
233
+ this .relationshipFields .add (propertyPathWrapper );
234
+ }
235
+
162
236
public NodeDescription <?> getNeo4jPersistentEntity () {
163
237
return neo4jPersistentEntity ;
164
238
}
165
239
166
240
public Map <String , Object > getParameters () {
167
241
return Collections .unmodifiableMap (parameters );
168
242
}
243
+
244
+ public Set <PropertyPathWrapper > getPropertyPathWrappers () {
245
+ return relationshipFields ;
246
+ }
169
247
}
0 commit comments