2222import  java .util .List ;
2323import  java .util .Optional ;
2424
25+ import  org .jetbrains .annotations .NotNull ;
26+ import  org .jspecify .annotations .NonNull ;
27+ import  org .jspecify .annotations .Nullable ;
28+ 
2529import  org .springframework .data .domain .Example ;
30+ import  org .springframework .data .domain .ExampleMatcher ;
2631import  org .springframework .data .mapping .PersistentPropertyAccessor ;
2732import  org .springframework .data .mapping .PropertyHandler ;
33+ import  org .springframework .data .mapping .PropertyPath ;
2834import  org .springframework .data .mapping .context .MappingContext ;
2935import  org .springframework .data .relational .core .mapping .RelationalPersistentEntity ;
3036import  org .springframework .data .relational .core .mapping .RelationalPersistentProperty ;
3137import  org .springframework .data .relational .core .query .Criteria ;
3238import  org .springframework .data .relational .core .query .Query ;
3339import  org .springframework .data .support .ExampleMatcherAccessor ;
3440import  org .springframework .util .Assert ;
41+ import  org .springframework .util .StringUtils ;
3542
3643/** 
3744 * Transform an {@link Example} into a {@link Query}. 
3845 * 
3946 * @since 2.2 
4047 * @author Greg Turnquist 
4148 * @author Jens Schauder 
49+  * @author Mikhail Polivakha 
4250 */ 
4351public  class  RelationalExampleMapper  {
4452
@@ -64,92 +72,194 @@ public <T> Query getMappedExample(Example<T> example) {
6472	 * {@link Query}. 
6573	 * 
6674	 * @param example 
67- 	 * @param entity  
75+ 	 * @param persistentEntity  
6876	 * @return query 
6977	 */ 
70- 	private  <T > Query  getMappedExample (Example <T > example , RelationalPersistentEntity <?> entity ) {
78+ 	private  <T > Query  getMappedExample (Example <T > example , RelationalPersistentEntity <?> persistentEntity ) {
7179
7280		Assert .notNull (example , "Example must not be null" );
73- 		Assert .notNull (entity , "RelationalPersistentEntity must not be null" );
81+ 		Assert .notNull (persistentEntity , "RelationalPersistentEntity must not be null" );
7482
75- 		PersistentPropertyAccessor <T > propertyAccessor  = entity .getPropertyAccessor (example .getProbe ());
83+ 		PersistentPropertyAccessor <T > probePropertyAccessor  = persistentEntity .getPropertyAccessor (example .getProbe ());
7684		ExampleMatcherAccessor  matcherAccessor  = new  ExampleMatcherAccessor (example .getMatcher ());
7785
78- 		final  List <Criteria > criteriaBasedOnProperties  = new  ArrayList <>();
86+ 		final  List <Criteria > criteriaBasedOnProperties  = buildCriteriaRecursive ( // 
87+ 				persistentEntity , // 
88+ 				matcherAccessor , // 
89+ 				probePropertyAccessor  // 
90+ 		);
7991
80- 		entity .doWithProperties ((PropertyHandler <RelationalPersistentProperty >) property  -> {
92+ 		// Criteria, assemble! 
93+ 		Criteria  criteria  = Criteria .empty ();
8194
82- 			if  (property .isCollectionLike () || property .isMap ()) {
83- 				return ;
84- 			}
95+ 		for  (Criteria  propertyCriteria  : criteriaBasedOnProperties ) {
8596
86- 			if  (matcherAccessor .isIgnoredPath (property .getName ())) {
87- 				return ;
97+ 			if  (example .getMatcher ().isAllMatching ()) {
98+ 				criteria  = criteria .and (propertyCriteria );
99+ 			} else  {
100+ 				criteria  = criteria .or (propertyCriteria );
88101			}
102+ 		}
103+ 
104+ 		return  Query .query (criteria );
105+ 	}
106+ 
107+ 	private  <T > @ NotNull  List <Criteria > buildCriteriaRecursive ( // 
108+ 			RelationalPersistentEntity <?> persistentEntity , // 
109+ 			ExampleMatcherAccessor  matcherAccessor , // 
110+ 			PersistentPropertyAccessor <T > probePropertyAccessor  // 
111+ 	) {
112+ 		final  List <Criteria > criteriaBasedOnProperties  = new  ArrayList <>();
113+ 
114+ 		persistentEntity .doWithProperties ((PropertyHandler <RelationalPersistentProperty >) property  -> {
115+ 			potentiallyEnrichCriteriaByProcessingProperty (
116+ 					null ,
117+ 					matcherAccessor ,  // 
118+ 					probePropertyAccessor , // 
119+ 					property , // 
120+ 					criteriaBasedOnProperties  // 
121+ 			);
122+ 		});
123+ 		return  criteriaBasedOnProperties ;
124+ 	}
125+ 
126+ 	/** 
127+ 	 * Analyzes the incoming {@code property} and potentially enriches the {@code criteriaBasedOnProperties} with the new 
128+ 	 * {@link Criteria} for this property. 
129+ 	 * <p> 
130+ 	 * This algorithm is recursive in order to take the embedded properties into account. The caller can expect that the result 
131+ 	 * of this method call is fully processed subtree of an aggreagte where the passed {@code property} serves as the root. 
132+ 	 * 
133+ 	 * @param propertyPath the {@link PropertyPath} of the passed {@code property}. 
134+ 	 * @param matcherAccessor the accessor for the original {@link ExampleMatcher}. 
135+ 	 * @param entityPropertiesAccessor the accessor for the properties of the current entity that holds the given {@code property} 
136+ 	 * @param property the property under analysis 
137+ 	 * @param criteriaBasedOnProperties the {@link List} of criteria objects that potentially gets enriched as a 
138+ 	 *                                  result of the incoming {@code property} processing 
139+ 	 */ 
140+ 	private  <T > void  potentiallyEnrichCriteriaByProcessingProperty (
141+ 			@ Nullable  PropertyPath  propertyPath ,
142+ 			ExampleMatcherAccessor  matcherAccessor , // 
143+ 			PersistentPropertyAccessor <T > entityPropertiesAccessor , // 
144+ 			RelationalPersistentProperty  property , // 
145+ 			List <Criteria > criteriaBasedOnProperties  // 
146+ 	) {
147+ 
148+ 		// QBE do not support queries on Child aggregates yet 
149+ 		if  (property .isCollectionLike () || property .isMap ()) {
150+ 			return ;
151+ 		}
152+ 
153+ 		PropertyPath  currentPropertyPath  = resolveCurrentPropertyPath (propertyPath , property );
154+ 		String  currentPropertyDotPath  = currentPropertyPath .toDotPath ();
155+ 
156+ 		if  (matcherAccessor .isIgnoredPath (currentPropertyDotPath )) {
157+ 			return ;
158+ 		}
89159
160+ 		Object  actualPropertyValue  = entityPropertiesAccessor .getProperty (property );
161+ 
162+ 		if  (property .isEmbedded () && actualPropertyValue  != null ) {
163+ 			processEmbeddedRecursively ( // 
164+ 					matcherAccessor , // 
165+ 					actualPropertyValue ,
166+ 					property , // 
167+ 					criteriaBasedOnProperties , // 
168+ 					currentPropertyPath  // 
169+ 			);
170+ 		} else  {
90171			Optional <?> optionalConvertedPropValue  = matcherAccessor  // 
91- 					.getValueTransformerForPath (property . getName () ) // 
92- 					.apply (Optional .ofNullable (propertyAccessor . getProperty ( property ) ));
172+ 					.getValueTransformerForPath (currentPropertyDotPath ) // 
173+ 					.apply (Optional .ofNullable (actualPropertyValue ));
93174
94175			// If the value is empty, don't try to match against it 
95- 			if  (! optionalConvertedPropValue .isPresent ()) {
176+ 			if  (optionalConvertedPropValue .isEmpty ()) {
96177				return ;
97178			}
98179
99180			Object  convPropValue  = optionalConvertedPropValue .get ();
100- 			boolean  ignoreCase  = matcherAccessor .isIgnoreCaseForPath (property . getName () );
181+ 			boolean  ignoreCase  = matcherAccessor .isIgnoreCaseForPath (currentPropertyDotPath );
101182
102183			String  column  = property .getName ();
103184
104- 			switch  (matcherAccessor .getStringMatcherForPath (property . getName () )) {
185+ 			switch  (matcherAccessor .getStringMatcherForPath (currentPropertyDotPath )) {
105186				case  DEFAULT :
106187				case  EXACT :
107- 					criteriaBasedOnProperties .add (includeNulls (example ) // 
188+ 					criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) // 
108189							? Criteria .where (column ).isNull ().or (column ).is (convPropValue ).ignoreCase (ignoreCase )
109190							: Criteria .where (column ).is (convPropValue ).ignoreCase (ignoreCase ));
110191					break ;
111192				case  ENDING :
112- 					criteriaBasedOnProperties .add (includeNulls (example ) // 
193+ 					criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) // 
113194							? Criteria .where (column ).isNull ().or (column ).like ("%"  + convPropValue ).ignoreCase (ignoreCase )
114195							: Criteria .where (column ).like ("%"  + convPropValue ).ignoreCase (ignoreCase ));
115196					break ;
116197				case  STARTING :
117- 					criteriaBasedOnProperties .add (includeNulls (example ) // 
198+ 					criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) // 
118199							? Criteria .where (column ).isNull ().or (column ).like (convPropValue  + "%" ).ignoreCase (ignoreCase )
119200							: Criteria .where (column ).like (convPropValue  + "%" ).ignoreCase (ignoreCase ));
120201					break ;
121202				case  CONTAINING :
122- 					criteriaBasedOnProperties .add (includeNulls (example ) // 
203+ 					criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) // 
123204							? Criteria .where (column ).isNull ().or (column ).like ("%"  + convPropValue  + "%" ).ignoreCase (ignoreCase )
124205							: Criteria .where (column ).like ("%"  + convPropValue  + "%" ).ignoreCase (ignoreCase ));
125206					break ;
126207				default :
127- 					throw  new  IllegalStateException (example . getMatcher () .getDefaultStringMatcher () + " is not supported" );
208+ 					throw  new  IllegalStateException (matcherAccessor .getDefaultStringMatcher () + " is not supported" );
128209			}
129- 		}); 
210+ 		}
130211
131- 		// Criteria, assemble! 
132- 		Criteria  criteria  = Criteria .empty ();
212+ 	}
133213
134- 		for  (Criteria  propertyCriteria  : criteriaBasedOnProperties ) {
214+ 	/** 
215+ 	 * Processes an embedded entity's properties recursively. 
216+ 	 * 
217+ 	 * @param matcherAccessor the input matcher on the {@link Example#getProbe() original probe}. 
218+ 	 * @param value the actual embedded object. 
219+ 	 * @param property the embedded property. 
220+ 	 * @param criteriaBasedOnProperties collection of {@link Criteria} objects to potentially enrich. 
221+ 	 * @param currentPropertyPath the dot-separated path of the passed {@code property}. 
222+ 	 */ 
223+ 	private  void  processEmbeddedRecursively (
224+ 			ExampleMatcherAccessor  matcherAccessor ,
225+ 			Object  value ,
226+ 			RelationalPersistentProperty  property ,
227+ 			List <Criteria > criteriaBasedOnProperties ,
228+ 			PropertyPath  currentPropertyPath 
229+ 	) {
230+ 		RelationalPersistentEntity <?> embeddedPersistentEntity  = mappingContext .getPersistentEntity (property .getTypeInformation ());
135231
136- 			if  (example .getMatcher ().isAllMatching ()) {
137- 				criteria  = criteria .and (propertyCriteria );
138- 			} else  {
139- 				criteria  = criteria .or (propertyCriteria );
140- 			}
141- 		}
232+ 		PersistentPropertyAccessor <?> embeddedEntityPropertyAccessor  = embeddedPersistentEntity .getPropertyAccessor (value );
142233
143- 		return  Query .query (criteria );
234+ 		embeddedPersistentEntity .doWithProperties ((PropertyHandler <RelationalPersistentProperty >) embeddedProperty  ->
235+ 						potentiallyEnrichCriteriaByProcessingProperty (
236+ 								currentPropertyPath ,
237+ 								matcherAccessor ,
238+ 								embeddedEntityPropertyAccessor ,
239+ 								embeddedProperty ,
240+ 								criteriaBasedOnProperties 
241+ 						)
242+ 		);
243+ 	}
244+ 
245+ 	@ NonNull 
246+ 	private  static  PropertyPath  resolveCurrentPropertyPath (@ Nullable  PropertyPath  propertyPath , RelationalPersistentProperty  property ) {
247+ 		PropertyPath  currentPropertyPath ;
248+ 
249+ 		if  (propertyPath  == null ) {
250+ 			currentPropertyPath  = PropertyPath .from (property .getName (), property .getOwner ().getTypeInformation ());
251+ 		} else  {
252+ 			currentPropertyPath  = propertyPath .nested (property .getName ());
253+ 		}
254+ 		return  currentPropertyPath ;
144255	}
145256
146257	/** 
147- 	 * Does this {@link Example } need to include {@literal NULL} values in its {@link Criteria}? 
258+ 	 * Does this {@link ExampleMatcherAccessor } need to include {@literal NULL} values in its {@link Criteria}? 
148259	 * 
149- 	 * @param example 
150- 	 * @return whether or not to include nulls. 
260+ 	 * @return whether to include nulls. 
151261	 */ 
152- 	private  static  <T > boolean  includeNulls (Example < T >  example ) {
153- 		return  example . getMatcher () .getNullHandler () == NullHandler .INCLUDE ;
262+ 	private  static  <T > boolean  includeNulls (ExampleMatcherAccessor   exampleMatcher ) {
263+ 		return  exampleMatcher .getNullHandler () == NullHandler .INCLUDE ;
154264	}
155265}
0 commit comments