22
22
import java .util .List ;
23
23
import java .util .Optional ;
24
24
25
+ import org .jspecify .annotations .NonNull ;
26
+ import org .jspecify .annotations .Nullable ;
27
+
25
28
import org .springframework .data .domain .Example ;
29
+ import org .springframework .data .domain .ExampleMatcher ;
26
30
import org .springframework .data .mapping .PersistentPropertyAccessor ;
27
31
import org .springframework .data .mapping .PropertyHandler ;
32
+ import org .springframework .data .mapping .PropertyPath ;
28
33
import org .springframework .data .mapping .context .MappingContext ;
29
34
import org .springframework .data .relational .core .mapping .RelationalPersistentEntity ;
30
35
import org .springframework .data .relational .core .mapping .RelationalPersistentProperty ;
39
44
* @since 2.2
40
45
* @author Greg Turnquist
41
46
* @author Jens Schauder
47
+ * @author Mikhail Polivakha
42
48
*/
43
49
public class RelationalExampleMapper {
44
50
@@ -64,92 +70,193 @@ public <T> Query getMappedExample(Example<T> example) {
64
70
* {@link Query}.
65
71
*
66
72
* @param example
67
- * @param entity
73
+ * @param persistentEntity
68
74
* @return query
69
75
*/
70
- private <T > Query getMappedExample (Example <T > example , RelationalPersistentEntity <?> entity ) {
76
+ private <T > Query getMappedExample (Example <T > example , RelationalPersistentEntity <?> persistentEntity ) {
71
77
72
78
Assert .notNull (example , "Example must not be null" );
73
- Assert .notNull (entity , "RelationalPersistentEntity must not be null" );
79
+ Assert .notNull (persistentEntity , "RelationalPersistentEntity must not be null" );
74
80
75
- PersistentPropertyAccessor <T > propertyAccessor = entity .getPropertyAccessor (example .getProbe ());
81
+ PersistentPropertyAccessor <T > probePropertyAccessor = persistentEntity .getPropertyAccessor (example .getProbe ());
76
82
ExampleMatcherAccessor matcherAccessor = new ExampleMatcherAccessor (example .getMatcher ());
77
83
78
- final List <Criteria > criteriaBasedOnProperties = new ArrayList <>();
84
+ final List <Criteria > criteriaBasedOnProperties = buildCriteria ( //
85
+ persistentEntity , //
86
+ matcherAccessor , //
87
+ probePropertyAccessor //
88
+ );
79
89
80
- entity .doWithProperties ((PropertyHandler <RelationalPersistentProperty >) property -> {
90
+ // Criteria, assemble!
91
+ Criteria criteria = Criteria .empty ();
81
92
82
- if (property .isCollectionLike () || property .isMap ()) {
83
- return ;
84
- }
93
+ for (Criteria propertyCriteria : criteriaBasedOnProperties ) {
85
94
86
- if (matcherAccessor .isIgnoredPath (property .getName ())) {
87
- return ;
95
+ if (example .getMatcher ().isAllMatching ()) {
96
+ criteria = criteria .and (propertyCriteria );
97
+ } else {
98
+ criteria = criteria .or (propertyCriteria );
88
99
}
100
+ }
101
+
102
+ return Query .query (criteria );
103
+ }
104
+
105
+ private <T > List <Criteria > buildCriteria ( //
106
+ RelationalPersistentEntity <?> persistentEntity , //
107
+ ExampleMatcherAccessor matcherAccessor , //
108
+ PersistentPropertyAccessor <T > probePropertyAccessor //
109
+ ) {
110
+ final List <Criteria > criteriaBasedOnProperties = new ArrayList <>();
111
+
112
+ persistentEntity .doWithProperties ((PropertyHandler <RelationalPersistentProperty >) property -> {
113
+ potentiallyEnrichCriteria (
114
+ null ,
115
+ matcherAccessor , //
116
+ probePropertyAccessor , //
117
+ property , //
118
+ criteriaBasedOnProperties //
119
+ );
120
+ });
121
+ return criteriaBasedOnProperties ;
122
+ }
123
+
124
+ /**
125
+ * Analyzes the incoming {@code property} and potentially enriches the {@code criteriaBasedOnProperties} with the new
126
+ * {@link Criteria} for this property.
127
+ * <p>
128
+ * This algorithm is recursive in order to take the embedded properties into account. The caller can expect that the result
129
+ * of this method call is fully processed subtree of an aggreagte where the passed {@code property} serves as the root.
130
+ *
131
+ * @param propertyPath the {@link PropertyPath} of the passed {@code property}.
132
+ * @param matcherAccessor the accessor for the original {@link ExampleMatcher}.
133
+ * @param entityPropertiesAccessor the accessor for the properties of the current entity that holds the given {@code property}
134
+ * @param property the property under analysis
135
+ * @param criteriaBasedOnProperties the {@link List} of criteria objects that potentially gets enriched as a
136
+ * result of the incoming {@code property} processing
137
+ */
138
+ private <T > void potentiallyEnrichCriteria (
139
+ @ Nullable PropertyPath propertyPath ,
140
+ ExampleMatcherAccessor matcherAccessor , //
141
+ PersistentPropertyAccessor <T > entityPropertiesAccessor , //
142
+ RelationalPersistentProperty property , //
143
+ List <Criteria > criteriaBasedOnProperties //
144
+ ) {
145
+
146
+ // QBE do not support queries on Child aggregates yet
147
+ if (property .isCollectionLike () || property .isMap ()) {
148
+ return ;
149
+ }
150
+
151
+ PropertyPath currentPropertyPath = resolveCurrentPropertyPath (propertyPath , property );
152
+ String currentPropertyDotPath = currentPropertyPath .toDotPath ();
153
+
154
+ if (matcherAccessor .isIgnoredPath (currentPropertyDotPath )) {
155
+ return ;
156
+ }
89
157
158
+ Object actualPropertyValue = entityPropertiesAccessor .getProperty (property );
159
+
160
+ if (property .isEmbedded () && actualPropertyValue != null ) {
161
+ processEmbeddedRecursively ( //
162
+ matcherAccessor , //
163
+ actualPropertyValue ,
164
+ property , //
165
+ criteriaBasedOnProperties , //
166
+ currentPropertyPath //
167
+ );
168
+ } else {
90
169
Optional <?> optionalConvertedPropValue = matcherAccessor //
91
- .getValueTransformerForPath (property . getName () ) //
92
- .apply (Optional .ofNullable (propertyAccessor . getProperty ( property ) ));
170
+ .getValueTransformerForPath (currentPropertyDotPath ) //
171
+ .apply (Optional .ofNullable (actualPropertyValue ));
93
172
94
173
// If the value is empty, don't try to match against it
95
- if (! optionalConvertedPropValue .isPresent ()) {
174
+ if (optionalConvertedPropValue .isEmpty ()) {
96
175
return ;
97
176
}
98
177
99
178
Object convPropValue = optionalConvertedPropValue .get ();
100
- boolean ignoreCase = matcherAccessor .isIgnoreCaseForPath (property . getName () );
179
+ boolean ignoreCase = matcherAccessor .isIgnoreCaseForPath (currentPropertyDotPath );
101
180
102
181
String column = property .getName ();
103
182
104
- switch (matcherAccessor .getStringMatcherForPath (property . getName () )) {
183
+ switch (matcherAccessor .getStringMatcherForPath (currentPropertyDotPath )) {
105
184
case DEFAULT :
106
185
case EXACT :
107
- criteriaBasedOnProperties .add (includeNulls (example ) //
186
+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
108
187
? Criteria .where (column ).isNull ().or (column ).is (convPropValue ).ignoreCase (ignoreCase )
109
188
: Criteria .where (column ).is (convPropValue ).ignoreCase (ignoreCase ));
110
189
break ;
111
190
case ENDING :
112
- criteriaBasedOnProperties .add (includeNulls (example ) //
191
+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
113
192
? Criteria .where (column ).isNull ().or (column ).like ("%" + convPropValue ).ignoreCase (ignoreCase )
114
193
: Criteria .where (column ).like ("%" + convPropValue ).ignoreCase (ignoreCase ));
115
194
break ;
116
195
case STARTING :
117
- criteriaBasedOnProperties .add (includeNulls (example ) //
196
+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
118
197
? Criteria .where (column ).isNull ().or (column ).like (convPropValue + "%" ).ignoreCase (ignoreCase )
119
198
: Criteria .where (column ).like (convPropValue + "%" ).ignoreCase (ignoreCase ));
120
199
break ;
121
200
case CONTAINING :
122
- criteriaBasedOnProperties .add (includeNulls (example ) //
201
+ criteriaBasedOnProperties .add (includeNulls (matcherAccessor ) //
123
202
? Criteria .where (column ).isNull ().or (column ).like ("%" + convPropValue + "%" ).ignoreCase (ignoreCase )
124
203
: Criteria .where (column ).like ("%" + convPropValue + "%" ).ignoreCase (ignoreCase ));
125
204
break ;
126
205
default :
127
- throw new IllegalStateException (example . getMatcher () .getDefaultStringMatcher () + " is not supported" );
206
+ throw new IllegalStateException (matcherAccessor .getDefaultStringMatcher () + " is not supported" );
128
207
}
129
- });
208
+ }
130
209
131
- // Criteria, assemble!
132
- Criteria criteria = Criteria .empty ();
210
+ }
133
211
134
- for (Criteria propertyCriteria : criteriaBasedOnProperties ) {
212
+ /**
213
+ * Processes an embedded entity's properties recursively.
214
+ *
215
+ * @param matcherAccessor the input matcher on the {@link Example#getProbe() original probe}.
216
+ * @param value the actual embedded object.
217
+ * @param property the embedded property.
218
+ * @param criteriaBasedOnProperties collection of {@link Criteria} objects to potentially enrich.
219
+ * @param currentPropertyPath the dot-separated path of the passed {@code property}.
220
+ */
221
+ private void processEmbeddedRecursively (
222
+ ExampleMatcherAccessor matcherAccessor ,
223
+ Object value ,
224
+ RelationalPersistentProperty property ,
225
+ List <Criteria > criteriaBasedOnProperties ,
226
+ PropertyPath currentPropertyPath
227
+ ) {
228
+ RelationalPersistentEntity <?> embeddedPersistentEntity = mappingContext .getPersistentEntity (property .getTypeInformation ());
135
229
136
- if (example .getMatcher ().isAllMatching ()) {
137
- criteria = criteria .and (propertyCriteria );
138
- } else {
139
- criteria = criteria .or (propertyCriteria );
140
- }
141
- }
230
+ PersistentPropertyAccessor <?> embeddedEntityPropertyAccessor = embeddedPersistentEntity .getPropertyAccessor (value );
142
231
143
- return Query .query (criteria );
232
+ embeddedPersistentEntity .doWithProperties ((PropertyHandler <RelationalPersistentProperty >) embeddedProperty ->
233
+ potentiallyEnrichCriteria (
234
+ currentPropertyPath ,
235
+ matcherAccessor ,
236
+ embeddedEntityPropertyAccessor ,
237
+ embeddedProperty ,
238
+ criteriaBasedOnProperties
239
+ )
240
+ );
241
+ }
242
+
243
+ private static PropertyPath resolveCurrentPropertyPath (@ Nullable PropertyPath propertyPath , RelationalPersistentProperty property ) {
244
+ PropertyPath currentPropertyPath ;
245
+
246
+ if (propertyPath == null ) {
247
+ currentPropertyPath = PropertyPath .from (property .getName (), property .getOwner ().getTypeInformation ());
248
+ } else {
249
+ currentPropertyPath = propertyPath .nested (property .getName ());
250
+ }
251
+ return currentPropertyPath ;
144
252
}
145
253
146
254
/**
147
- * Does this {@link Example } need to include {@literal NULL} values in its {@link Criteria}?
255
+ * Does this {@link ExampleMatcherAccessor } need to include {@literal NULL} values in its {@link Criteria}?
148
256
*
149
- * @param example
150
- * @return whether or not to include nulls.
257
+ * @return whether to include nulls.
151
258
*/
152
- private static <T > boolean includeNulls (Example < T > example ) {
153
- return example . getMatcher () .getNullHandler () == NullHandler .INCLUDE ;
259
+ private static <T > boolean includeNulls (ExampleMatcherAccessor exampleMatcher ) {
260
+ return exampleMatcher .getNullHandler () == NullHandler .INCLUDE ;
154
261
}
155
262
}
0 commit comments