2222import java .util .Collection ;
2323import java .util .Collections ;
2424import java .util .HashMap ;
25+ import java .util .HashSet ;
2526import java .util .Map ;
2627import java .util .Optional ;
28+ import java .util .Set ;
29+ import java .util .concurrent .atomic .AtomicInteger ;
2730import java .util .function .BiFunction ;
2831
2932import org .neo4j .cypherdsl .core .Condition ;
3336import org .neo4j .cypherdsl .core .StatementBuilder ;
3437import org .springframework .data .domain .Example ;
3538import org .springframework .data .domain .ExampleMatcher ;
39+ import org .springframework .data .mapping .PropertyPath ;
3640import org .springframework .data .neo4j .core .convert .Neo4jConversionService ;
3741import org .springframework .data .neo4j .core .mapping .Constants ;
3842import org .springframework .data .neo4j .core .mapping .GraphPropertyDescription ;
3943import org .springframework .data .neo4j .core .mapping .Neo4jMappingContext ;
4044import org .springframework .data .neo4j .core .mapping .Neo4jPersistentEntity ;
4145import org .springframework .data .neo4j .core .mapping .Neo4jPersistentProperty ;
4246import org .springframework .data .neo4j .core .mapping .NodeDescription ;
47+ import org .springframework .data .neo4j .core .mapping .RelationshipDescription ;
4348import org .springframework .data .support .ExampleMatcherAccessor ;
4449import org .springframework .data .util .DirectFieldAccessFallbackBeanWrapper ;
50+ import org .springframework .lang .Nullable ;
4551
4652/**
4753 * Support class for "query by example" executors.
@@ -56,80 +62,142 @@ final class Predicate {
5662
5763 static <S > Predicate create (Neo4jMappingContext mappingContext , Example <S > example ) {
5864
59- Neo4jPersistentEntity <?> probeNodeDescription = mappingContext .getRequiredPersistentEntity (example .getProbeType ());
65+ Neo4jPersistentEntity <?> nodeDescription = mappingContext .getRequiredPersistentEntity (example .getProbeType ());
6066
61- Collection <GraphPropertyDescription > graphProperties = probeNodeDescription .getGraphProperties ();
67+ Collection <GraphPropertyDescription > graphProperties = nodeDescription .getGraphProperties ();
6268 DirectFieldAccessFallbackBeanWrapper beanWrapper = new DirectFieldAccessFallbackBeanWrapper (example .getProbe ());
6369 ExampleMatcher matcher = example .getMatcher ();
6470 ExampleMatcher .MatchMode mode = matcher .getMatchMode ();
6571 ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor (matcher );
72+ AtomicInteger relationshipPatternCount = new AtomicInteger ();
6673
67- Predicate predicate = new Predicate (probeNodeDescription );
74+ Predicate predicate = new Predicate (nodeDescription );
6875 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+ }
6986
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 ) {
7190
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 ) {
7496 continue ;
7597 }
7698
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 ();
79110
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 ());
84113
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 );
90127 }
91128
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+ }
93143
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 ) {
97164 predicate .add (mode ,
98165 predicate .neo4jPersistentEntity .getIdExpression ().isEqualTo (literalOf (optionalValue .get ())));
99166 } 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 );
120180 }
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+ };
129191 }
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 ());
130200 }
131-
132- return predicate ;
133201 }
134202
135203 private final Neo4jPersistentEntity neo4jPersistentEntity ;
@@ -138,6 +206,8 @@ static <S> Predicate create(Neo4jMappingContext mappingContext, Example<S> examp
138206
139207 private final Map <String , Object > parameters = new HashMap <>();
140208
209+ private final Set <PropertyPathWrapper > relationshipFields = new HashSet <>();
210+
141211 private Predicate (Neo4jPersistentEntity neo4jPersistentEntity ) {
142212 this .neo4jPersistentEntity = neo4jPersistentEntity ;
143213 }
@@ -159,11 +229,19 @@ private void add(ExampleMatcher.MatchMode matchMode, Condition additionalConditi
159229 };
160230 }
161231
232+ private void addRelationship (PropertyPathWrapper propertyPathWrapper ) {
233+ this .relationshipFields .add (propertyPathWrapper );
234+ }
235+
162236 public NodeDescription <?> getNeo4jPersistentEntity () {
163237 return neo4jPersistentEntity ;
164238 }
165239
166240 public Map <String , Object > getParameters () {
167241 return Collections .unmodifiableMap (parameters );
168242 }
243+
244+ public Set <PropertyPathWrapper > getPropertyPathWrappers () {
245+ return relationshipFields ;
246+ }
169247}
0 commit comments