1616package org .springframework .data .neo4j .core ;
1717
1818import java .beans .PropertyDescriptor ;
19+ import java .util .ArrayList ;
1920import java .util .Collection ;
2021import java .util .Collections ;
2122import java .util .HashSet ;
2223import java .util .Objects ;
2324import java .util .Optional ;
25+ import java .util .Set ;
26+ import java .util .concurrent .locks .ReentrantLock ;
27+ import java .util .function .Predicate ;
2428
2529import org .apiguardian .api .API ;
2630
2933import org .springframework .data .neo4j .core .mapping .GraphPropertyDescription ;
3034import org .springframework .data .neo4j .core .mapping .Neo4jMappingContext ;
3135import org .springframework .data .neo4j .core .mapping .Neo4jPersistentEntity ;
36+ import org .springframework .data .neo4j .core .mapping .NodeDescription ;
3237import org .springframework .data .neo4j .core .mapping .PropertyFilter ;
3338import org .springframework .data .neo4j .core .mapping .RelationshipDescription ;
3439import org .springframework .data .projection .ProjectionFactory ;
4752@ API (status = API .Status .INTERNAL , since = "6.1.3" )
4853public final class PropertyFilterSupport {
4954
55+ // A cache to look up if there are aggregate boundaries between two entities.
56+ private static final AggregateBoundaries AGGREGATE_BOUNDARIES = new AggregateBoundaries ();
57+ 5058 private PropertyFilterSupport () {
5159 }
5260
@@ -61,6 +69,11 @@ public static Collection<PropertyFilter.ProjectedPath> getInputProperties(Result
6169
6270 boolean isProjecting = returnedType .isProjecting ();
6371 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+ }
6477
6578 if (!isProjecting || !isClosedProjection ) {
6679 return Collections .emptySet ();
@@ -84,6 +97,160 @@ public static Collection<PropertyFilter.ProjectedPath> getInputProperties(Result
8497 return filteredProperties ;
8598 }
8699
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+ 87254 static Collection <PropertyFilter .ProjectedPath > addPropertiesFrom (Class <?> domainType , Class <?> returnType ,
88255 ProjectionFactory projectionFactory , Neo4jMappingContext neo4jMappingContext ) {
89256
@@ -258,4 +425,65 @@ boolean isChildLevel() {
258425
259426 }
260427
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+ 261489}
0 commit comments