16
16
package org .springframework .data .neo4j .core ;
17
17
18
18
import java .beans .PropertyDescriptor ;
19
+ import java .util .ArrayList ;
19
20
import java .util .Collection ;
20
21
import java .util .Collections ;
21
22
import java .util .HashSet ;
22
23
import java .util .Objects ;
23
24
import java .util .Optional ;
25
+ import java .util .Set ;
26
+ import java .util .concurrent .locks .ReentrantLock ;
27
+ import java .util .function .Predicate ;
24
28
25
29
import org .apiguardian .api .API ;
26
30
29
33
import org .springframework .data .neo4j .core .mapping .GraphPropertyDescription ;
30
34
import org .springframework .data .neo4j .core .mapping .Neo4jMappingContext ;
31
35
import org .springframework .data .neo4j .core .mapping .Neo4jPersistentEntity ;
36
+ import org .springframework .data .neo4j .core .mapping .NodeDescription ;
32
37
import org .springframework .data .neo4j .core .mapping .PropertyFilter ;
33
38
import org .springframework .data .neo4j .core .mapping .RelationshipDescription ;
34
39
import org .springframework .data .projection .ProjectionFactory ;
47
52
@ API (status = API .Status .INTERNAL , since = "6.1.3" )
48
53
public final class PropertyFilterSupport {
49
54
55
+ // A cache to look up if there are aggregate boundaries between two entities.
56
+ private static final AggregateBoundaries AGGREGATE_BOUNDARIES = new AggregateBoundaries ();
57
+
50
58
private PropertyFilterSupport () {
51
59
}
52
60
@@ -61,6 +69,11 @@ public static Collection<PropertyFilter.ProjectedPath> getInputProperties(Result
61
69
62
70
boolean isProjecting = returnedType .isProjecting ();
63
71
boolean isClosedProjection = factory .getProjectionInformation (potentiallyProjectedType ).isClosed ();
72
+ if (!isProjecting && containsAggregateBoundary (domainType , mappingContext )) {
73
+ Collection <PropertyFilter .ProjectedPath > listForAggregate = createListForAggregate (domainType ,
74
+ mappingContext );
75
+ return listForAggregate ;
76
+ }
64
77
65
78
if (!isProjecting || !isClosedProjection ) {
66
79
return Collections .emptySet ();
@@ -84,6 +97,160 @@ public static Collection<PropertyFilter.ProjectedPath> getInputProperties(Result
84
97
return filteredProperties ;
85
98
}
86
99
100
+ public static Collection <PropertyFilter .ProjectedPath > getInputPropertiesForAggregateBoundary (Class <?> domainType ,
101
+ Neo4jMappingContext mappingContext ) {
102
+ if (!containsAggregateBoundary (domainType , mappingContext )) {
103
+ return Collections .emptySet ();
104
+ }
105
+ Collection <PropertyFilter .ProjectedPath > listForAggregate = createListForAggregate (domainType , mappingContext );
106
+ return listForAggregate ;
107
+ }
108
+
109
+ public static Predicate <PropertyFilter .RelaxedPropertyPath > createRelaxedPropertyPathFilter (Class <?> domainType ,
110
+ Neo4jMappingContext mappingContext ) {
111
+ if (!containsAggregateBoundary (domainType , mappingContext )) {
112
+ return PropertyFilter .NO_FILTER ;
113
+ }
114
+ Collection <PropertyFilter .RelaxedPropertyPath > relaxedPropertyPathFilter = createRelaxedPropertyPathFilter (
115
+ domainType , mappingContext , new HashSet <RelationshipDescription >());
116
+ return (rpp ) -> {
117
+ return relaxedPropertyPathFilter .contains (rpp );
118
+ };
119
+ }
120
+
121
+ private static Collection <PropertyFilter .RelaxedPropertyPath > createRelaxedPropertyPathFilter (Class <?> domainType ,
122
+ Neo4jMappingContext neo4jMappingContext , Set <RelationshipDescription > processedRelationships ) {
123
+ var relaxedPropertyPath = PropertyFilter .RelaxedPropertyPath .withRootType (domainType );
124
+ var relaxedPropertyPaths = new ArrayList <PropertyFilter .RelaxedPropertyPath >();
125
+ relaxedPropertyPaths .add (relaxedPropertyPath );
126
+ Neo4jPersistentEntity <?> domainEntity = neo4jMappingContext .getRequiredPersistentEntity (domainType );
127
+ domainEntity .getGraphProperties ().stream ().forEach (property -> {
128
+ relaxedPropertyPaths .add (relaxedPropertyPath .append (property .getFieldName ()));
129
+ });
130
+ for (RelationshipDescription relationshipDescription : domainEntity .getRelationshipsInHierarchy (any -> true )) {
131
+ var target = relationshipDescription .getTarget ();
132
+ PropertyFilter .RelaxedPropertyPath relationshipPath = relaxedPropertyPath
133
+ .append (relationshipDescription .getFieldName ());
134
+ relaxedPropertyPaths .add (relationshipPath );
135
+ processedRelationships .add (relationshipDescription );
136
+ createRelaxedPropertyPathFilter (domainType , target , relationshipPath , relaxedPropertyPaths ,
137
+ processedRelationships );
138
+ }
139
+ return relaxedPropertyPaths ;
140
+ }
141
+
142
+ private static Collection <PropertyFilter .RelaxedPropertyPath > createRelaxedPropertyPathFilter (Class <?> domainType ,
143
+ NodeDescription <?> nodeDescription , PropertyFilter .RelaxedPropertyPath relaxedPropertyPath ,
144
+ Collection <PropertyFilter .RelaxedPropertyPath > relaxedPropertyPaths ,
145
+ Set <RelationshipDescription > processedRelationships ) {
146
+ // always add the related entity itself
147
+ relaxedPropertyPaths .add (relaxedPropertyPath );
148
+ if (nodeDescription .hasAggregateBoundaries (domainType )) {
149
+ relaxedPropertyPaths .add (relaxedPropertyPath
150
+ .append (((Neo4jPersistentEntity <?>) nodeDescription ).getRequiredIdProperty ().getFieldName ()));
151
+
152
+ return relaxedPropertyPaths ;
153
+ }
154
+ nodeDescription .getGraphProperties ().stream ().forEach (property -> {
155
+ relaxedPropertyPaths .add (relaxedPropertyPath .append (property .getFieldName ()));
156
+ });
157
+ for (RelationshipDescription relationshipDescription : nodeDescription
158
+ .getRelationshipsInHierarchy (any -> true )) {
159
+ if (processedRelationships .contains (relationshipDescription )) {
160
+ continue ;
161
+ }
162
+ var target = relationshipDescription .getTarget ();
163
+ PropertyFilter .RelaxedPropertyPath relationshipPath = relaxedPropertyPath
164
+ .append (relationshipDescription .getFieldName ());
165
+ relaxedPropertyPaths .add (relationshipPath );
166
+ processedRelationships .add (relationshipDescription );
167
+ createRelaxedPropertyPathFilter (domainType , target , relationshipPath , relaxedPropertyPaths ,
168
+ processedRelationships );
169
+ }
170
+ return relaxedPropertyPaths ;
171
+ }
172
+
173
+ private static Collection <PropertyFilter .ProjectedPath > createListForAggregate (Class <?> domainType ,
174
+ Neo4jMappingContext neo4jMappingContext ) {
175
+ var relaxedPropertyPath = PropertyFilter .RelaxedPropertyPath .withRootType (domainType );
176
+ var filteredProperties = new ArrayList <PropertyFilter .ProjectedPath >();
177
+ Neo4jPersistentEntity <?> domainEntity = neo4jMappingContext .getRequiredPersistentEntity (domainType );
178
+ domainEntity .getGraphProperties ().stream ().forEach (property -> {
179
+ filteredProperties
180
+ .add (new PropertyFilter .ProjectedPath (relaxedPropertyPath .append (property .getFieldName ()), false ));
181
+ });
182
+ for (RelationshipDescription relationshipDescription : domainEntity .getRelationshipsInHierarchy (any -> true )) {
183
+ var target = relationshipDescription .getTarget ();
184
+ filteredProperties .addAll (createListForAggregate (domainType , target ,
185
+ relaxedPropertyPath .append (relationshipDescription .getFieldName ())));
186
+ }
187
+ return filteredProperties ;
188
+ }
189
+
190
+ private static Collection <PropertyFilter .ProjectedPath > createListForAggregate (Class <?> domainType ,
191
+ NodeDescription <?> nodeDescription , PropertyFilter .RelaxedPropertyPath relaxedPropertyPath ) {
192
+ var filteredProperties = new ArrayList <PropertyFilter .ProjectedPath >();
193
+ // always add the related entity itself
194
+ filteredProperties .add (new PropertyFilter .ProjectedPath (relaxedPropertyPath , false ));
195
+ if (nodeDescription .hasAggregateBoundaries (domainType )) {
196
+ filteredProperties .add (new PropertyFilter .ProjectedPath (
197
+ relaxedPropertyPath
198
+ .append (((Neo4jPersistentEntity <?>) nodeDescription ).getRequiredIdProperty ().getFieldName ()),
199
+ false ));
200
+ return filteredProperties ;
201
+ }
202
+ nodeDescription .getGraphProperties ().stream ().forEach (property -> {
203
+ filteredProperties
204
+ .add (new PropertyFilter .ProjectedPath (relaxedPropertyPath .append (property .getFieldName ()), false ));
205
+ });
206
+ for (RelationshipDescription relationshipDescription : nodeDescription
207
+ .getRelationshipsInHierarchy (any -> true )) {
208
+ var target = relationshipDescription .getTarget ();
209
+ filteredProperties .addAll (createListForAggregate (domainType , target ,
210
+ relaxedPropertyPath .append (relationshipDescription .getFieldName ())));
211
+ }
212
+ return filteredProperties ;
213
+ }
214
+
215
+ private static boolean containsAggregateBoundary (Class <?> domainType , Neo4jMappingContext neo4jMappingContext ) {
216
+ var processedRelationships = new HashSet <RelationshipDescription >();
217
+ Neo4jPersistentEntity <?> domainEntity = neo4jMappingContext .getRequiredPersistentEntity (domainType );
218
+ if (AGGREGATE_BOUNDARIES .hasEntry (domainEntity , domainType )) {
219
+ return AGGREGATE_BOUNDARIES .getCachedStatus (domainEntity , domainType );
220
+ }
221
+ for (RelationshipDescription relationshipDescription : domainEntity .getRelationshipsInHierarchy (any -> true )) {
222
+ var target = relationshipDescription .getTarget ();
223
+ if (target .hasAggregateBoundaries (domainType )) {
224
+ AGGREGATE_BOUNDARIES .add (domainEntity , domainType , true );
225
+ return true ;
226
+ }
227
+ processedRelationships .add (relationshipDescription );
228
+ boolean containsAggregateBoundary = containsAggregateBoundary (domainType , target , processedRelationships );
229
+ AGGREGATE_BOUNDARIES .add (domainEntity , domainType , containsAggregateBoundary );
230
+ return containsAggregateBoundary ;
231
+ }
232
+ AGGREGATE_BOUNDARIES .add (domainEntity , domainType , false );
233
+ return false ;
234
+ }
235
+
236
+ private static boolean containsAggregateBoundary (Class <?> domainType , NodeDescription <?> nodeDescription ,
237
+ Set <RelationshipDescription > processedRelationships ) {
238
+ for (RelationshipDescription relationshipDescription : nodeDescription
239
+ .getRelationshipsInHierarchy (any -> true )) {
240
+ var target = relationshipDescription .getTarget ();
241
+ Class <?> underlyingClass = nodeDescription .getUnderlyingClass ();
242
+ if (processedRelationships .contains (relationshipDescription )) {
243
+ continue ;
244
+ }
245
+ if (target .hasAggregateBoundaries (domainType )) {
246
+ return true ;
247
+ }
248
+ processedRelationships .add (relationshipDescription );
249
+ return containsAggregateBoundary (domainType , target , processedRelationships );
250
+ }
251
+ return false ;
252
+ }
253
+
87
254
static Collection <PropertyFilter .ProjectedPath > addPropertiesFrom (Class <?> domainType , Class <?> returnType ,
88
255
ProjectionFactory projectionFactory , Neo4jMappingContext neo4jMappingContext ) {
89
256
@@ -258,4 +425,65 @@ boolean isChildLevel() {
258
425
259
426
}
260
427
428
+ record AggregateBoundary (Neo4jPersistentEntity <?> entity , Class <?> domainType , boolean status ) {
429
+
430
+ }
431
+
432
+ private static final class AggregateBoundaries {
433
+
434
+ private final Set <AggregateBoundary > aggregateBoundaries = new HashSet <>();
435
+
436
+ private final ReentrantLock lock = new ReentrantLock ();
437
+
438
+ void add (Neo4jPersistentEntity <?> entity , Class <?> domainType , boolean status ) {
439
+ try {
440
+ this .lock .lock ();
441
+ for (AggregateBoundary aggregateBoundary : this .aggregateBoundaries ) {
442
+ if (aggregateBoundary .domainType ().equals (domainType ) && aggregateBoundary .entity ().equals (entity )
443
+ && aggregateBoundary .status () != status ) {
444
+ throw new IllegalStateException ("%s cannot have a different status to %s. Was %s now %s"
445
+ .formatted (entity .getUnderlyingClass (), domainType , aggregateBoundary .status (), status ));
446
+ }
447
+ }
448
+ this .aggregateBoundaries .add (new AggregateBoundary (entity , domainType , status ));
449
+ }
450
+ finally {
451
+ this .lock .unlock ();
452
+ }
453
+ }
454
+
455
+ boolean hasEntry (Neo4jPersistentEntity <?> entity , Class <?> domainType ) {
456
+ try {
457
+ this .lock .lock ();
458
+ for (AggregateBoundary aggregateBoundary : this .aggregateBoundaries ) {
459
+ if (aggregateBoundary .domainType ().equals (domainType )
460
+ && aggregateBoundary .entity ().equals (entity )) {
461
+ return true ;
462
+ }
463
+ }
464
+ return false ;
465
+ }
466
+ finally {
467
+ this .lock .unlock ();
468
+ }
469
+ }
470
+
471
+ boolean getCachedStatus (Neo4jPersistentEntity <?> entity , Class <?> domainType ) {
472
+ try {
473
+ this .lock .lock ();
474
+ for (AggregateBoundary aggregateBoundary : this .aggregateBoundaries ) {
475
+ if (aggregateBoundary .domainType ().equals (domainType )
476
+ && aggregateBoundary .entity ().equals (entity )) {
477
+ return aggregateBoundary .status ();
478
+ }
479
+ }
480
+ return false ;
481
+ }
482
+ finally {
483
+ this .lock .unlock ();
484
+ }
485
+ }
486
+
487
+ }
488
+
261
489
}
0 commit comments