15
15
*/
16
16
package org .springframework .data .jpa .repository .query ;
17
17
18
+ import net .sf .jsqlparser .JSQLParserException ;
19
+ import net .sf .jsqlparser .expression .Alias ;
20
+ import net .sf .jsqlparser .expression .Expression ;
21
+ import net .sf .jsqlparser .expression .Function ;
22
+ import net .sf .jsqlparser .expression .operators .conditional .AndExpression ;
23
+ import net .sf .jsqlparser .expression .operators .relational .EqualsTo ;
24
+ import net .sf .jsqlparser .parser .CCJSqlParserUtil ;
25
+ import net .sf .jsqlparser .schema .Column ;
26
+ import net .sf .jsqlparser .schema .Table ;
27
+ import net .sf .jsqlparser .statement .select .*;
28
+ import net .sf .jsqlparser .util .SelectUtils ;
18
29
import org .springframework .data .domain .Sort ;
30
+ import org .springframework .data .util .Streamable ;
31
+ import org .springframework .util .Assert ;
32
+ import org .springframework .util .CollectionUtils ;
33
+ import org .springframework .util .StringUtils ;
34
+
35
+ import java .util .*;
36
+ import java .util .stream .Collectors ;
37
+
38
+ import static org .springframework .data .jpa .repository .query .JSqlParserUtils .*;
39
+ import static org .springframework .data .jpa .repository .query .QueryUtils .checkSortExpression ;
19
40
20
41
/**
21
- * The implementation of {@link QueryEnhancer} using {@link JSqlParserQueryUtils} .
42
+ * The implementation of {@link QueryEnhancer} using JSqlParser .
22
43
*
23
44
* @author Diego Krupitza
24
45
*/
25
46
public class JSqlParserQueryEnhancer implements QueryEnhancer {
26
47
48
+ private static final String DEFAULT_TABLE_ALIAS = "x" ;
49
+
27
50
private final DeclaredQuery query ;
28
51
29
52
/**
@@ -35,32 +58,275 @@ public JSqlParserQueryEnhancer(DeclaredQuery query) {
35
58
36
59
@ Override
37
60
public String getExistsQueryString (String entityName , String countQueryPlaceHolder , Iterable <String > idAttributes ) {
38
- return JSqlParserQueryUtils .getExistsQueryString (entityName , countQueryPlaceHolder , idAttributes );
61
+ final Table tableNameWithAlias = getTableWithAlias (entityName , DEFAULT_TABLE_ALIAS );
62
+ Function jSqlCount = getJSqlCount (Collections .singletonList (countQueryPlaceHolder ), false );
63
+
64
+ Select select = SelectUtils .buildSelectFromTableAndSelectItems (tableNameWithAlias ,
65
+ new SelectExpressionItem (jSqlCount ));
66
+
67
+ PlainSelect selectBody = (PlainSelect ) select .getSelectBody ();
68
+
69
+ List <Expression > equalityExpressions = Streamable .of (idAttributes ).stream () //
70
+ .map (field -> {
71
+ Expression tableNameField = new Column ().withTable (tableNameWithAlias ).withColumnName (field );
72
+ Expression inputField = new Column (":" .concat (field ));
73
+ return new EqualsTo (tableNameField , inputField );
74
+ }).collect (Collectors .toList ());
75
+
76
+ if (equalityExpressions .size () > 1 ) {
77
+ AndExpression rootOfWhereClause = concatenateWithAndExpression (equalityExpressions );
78
+ selectBody .setWhere (rootOfWhereClause );
79
+ } else if (equalityExpressions .size () == 1 ) {
80
+ selectBody .setWhere (equalityExpressions .get (0 ));
81
+ }
82
+
83
+ return selectBody .toString ();
39
84
}
40
85
41
86
@ Override
42
87
public String getQueryString (String template , String entityName ) {
43
- return JSqlParserQueryUtils .getQueryString (template , entityName );
88
+ Assert .hasText (entityName , "Entity name must not be null or empty!" );
89
+ return String .format (template , entityName );
44
90
}
45
91
46
92
@ Override
47
93
public String applySorting (Sort sort , String alias ) {
48
- return JSqlParserQueryUtils .applySorting (this .query .getQueryString (), sort , alias );
94
+ String queryString = query .getQueryString ();
95
+ Assert .hasText (queryString , "Query must not be null or empty!" );
96
+
97
+ if (sort .isUnsorted ()) {
98
+ return queryString ;
99
+ }
100
+
101
+ Select selectStatement = parseSelectStatement (queryString );
102
+ PlainSelect selectBody = (PlainSelect ) selectStatement .getSelectBody ();
103
+
104
+ final Set <String > joinAliases = getJoinAliases (selectBody );
105
+
106
+ final Set <String > selectionAliases = getSelectionAliases (selectBody );
107
+
108
+ List <OrderByElement > orderByElements = sort .stream () //
109
+ .map (order -> getOrderClause (joinAliases , selectionAliases , alias , order )) //
110
+ .collect (Collectors .toList ());
111
+
112
+ if (CollectionUtils .isEmpty (selectBody .getOrderByElements ())) {
113
+ selectBody .setOrderByElements (new ArrayList <>());
114
+ }
115
+
116
+ selectBody .getOrderByElements ().addAll (orderByElements );
117
+
118
+ return selectBody .toString ();
119
+
120
+ }
121
+
122
+ /**
123
+ * Returns the aliases used inside the selection part in the query.
124
+ *
125
+ * @param selectBody a {@link PlainSelect} containing a query. Must not be {@literal null}.
126
+ * @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
127
+ */
128
+ private Set <String > getSelectionAliases (PlainSelect selectBody ) {
129
+
130
+ if (CollectionUtils .isEmpty (selectBody .getSelectItems ())) {
131
+ return new HashSet <>();
132
+ }
133
+
134
+ return selectBody .getSelectItems ().stream () //
135
+ .filter (SelectExpressionItem .class ::isInstance ) //
136
+ .map (item -> ((SelectExpressionItem ) item ).getAlias ()) //
137
+ .filter (Objects ::nonNull ) //
138
+ .map (Alias ::getName ) //
139
+ .collect (Collectors .toSet ());
140
+ }
141
+
142
+ /**
143
+ * Returns the aliases used for {@code join}s.
144
+ *
145
+ * @param query a query string to extract the aliases of joins from. Must not be {@literal null}.
146
+ * @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}.
147
+ */
148
+ private Set <String > getJoinAliases (String query ) {
149
+ return getJoinAliases ((PlainSelect ) parseSelectStatement (query ).getSelectBody ());
150
+ }
151
+
152
+ /**
153
+ * Returns the aliases used for {@code join}s.
154
+ *
155
+ * @param selectBody the selection body to extract the aliases of joins from. Must not be {@literal null}.
156
+ * @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}.
157
+ */
158
+ private Set <String > getJoinAliases (PlainSelect selectBody ) {
159
+
160
+ if (CollectionUtils .isEmpty (selectBody .getJoins ())) {
161
+ return new HashSet <>();
162
+ }
163
+
164
+ return selectBody .getJoins ().stream () //
165
+ .map (join -> join .getRightItem ().getAlias ()) //
166
+ .filter (Objects ::nonNull ) //
167
+ .map (Alias ::getName ) //
168
+ .collect (Collectors .toSet ());
169
+ }
170
+
171
+ /**
172
+ * Returns the order clause for the given {@link Sort.Order}. Will prefix the clause with the given alias if the
173
+ * referenced property refers to a join alias, i.e. starts with {@code $alias.}.
174
+ *
175
+ * @param joinAliases the join aliases of the original query. Must not be {@literal null}.
176
+ * @param alias the alias for the root entity. May be {@literal null}.
177
+ * @param order the order object to build the clause for. Must not be {@literal null}.
178
+ * @return a {@link OrderByElement} containing an order clause. Guaranteed to be not {@literal null}.
179
+ */
180
+ private OrderByElement getOrderClause (final Set <String > joinAliases , final Set <String > selectionAliases ,
181
+ final String alias , final Sort .Order order ) {
182
+
183
+ final OrderByElement orderByElement = new OrderByElement ();
184
+ orderByElement .setAsc (order .getDirection ().isAscending ());
185
+ orderByElement .setAscDescPresent (true );
186
+
187
+ final String property = order .getProperty ();
188
+
189
+ checkSortExpression (order );
190
+
191
+ if (selectionAliases .contains (property )) {
192
+ Expression orderExpression = order .isIgnoreCase () ? getJSqlLower (property ) : new Column (property );
193
+
194
+ orderByElement .setExpression (orderExpression );
195
+ return orderByElement ;
196
+ }
197
+
198
+ boolean qualifyReference = joinAliases //
199
+ .parallelStream () //
200
+ .map (joinAlias -> joinAlias .concat ("." )) //
201
+ .noneMatch (property ::startsWith );
202
+
203
+ boolean functionIndicator = property .contains ("(" );
204
+
205
+ String reference = qualifyReference && !functionIndicator && StringUtils .hasText (alias )
206
+ ? String .format ("%s.%s" , alias , property )
207
+ : property ;
208
+ Expression orderExpression = order .isIgnoreCase () ? getJSqlLower (reference ) : new Column (reference );
209
+ orderByElement .setExpression (orderExpression );
210
+ return orderByElement ;
49
211
}
50
212
51
213
@ Override
52
214
public String detectAlias () {
53
- return JSqlParserQueryUtils .detectAlias (this .query .getQueryString ());
215
+ return detectAlias (this .query .getQueryString ());
216
+ }
217
+
218
+ /**
219
+ * Resolves the alias for the entity to be retrieved from the given JPA query. Note that you only provide valid Query
220
+ * strings. Things such as <code>from User u</code> will throw an {@link IllegalArgumentException}.
221
+ *
222
+ * @param query must not be {@literal null}.
223
+ * @return Might return {@literal null}.
224
+ */
225
+ private String detectAlias (String query ) {
226
+ Select selectStatement = parseSelectStatement (query );
227
+ PlainSelect selectBody = (PlainSelect ) selectStatement .getSelectBody ();
228
+ return detectAlias (selectBody );
229
+ }
230
+
231
+ /**
232
+ * Resolves the alias for the entity to be retrieved from the given {@link PlainSelect}. Note that you only provide
233
+ * valid Query strings. Things such as <code>from User u</code> will throw an {@link IllegalArgumentException}.
234
+ *
235
+ * @param selectBody must not be {@literal null}.
236
+ * @return Might return {@literal null}.
237
+ */
238
+ private static String detectAlias (PlainSelect selectBody ) {
239
+ Alias alias = selectBody .getFromItem ().getAlias ();
240
+ return alias == null ? null : alias .getName ();
54
241
}
55
242
56
243
@ Override
57
244
public String createCountQueryFor (String countProjection ) {
58
- return JSqlParserQueryUtils .createCountQueryFor (this .query .getQueryString (), countProjection );
245
+
246
+ Assert .hasText (this .query .getQueryString (), "OriginalQuery must not be null or empty!" );
247
+
248
+ Select selectStatement = parseSelectStatement (this .query .getQueryString ());
249
+ PlainSelect selectBody = (PlainSelect ) selectStatement .getSelectBody ();
250
+
251
+ // remove order by
252
+ selectBody .setOrderByElements (null );
253
+
254
+ if (StringUtils .hasText (countProjection )) {
255
+ Function jSqlCount = getJSqlCount (Collections .singletonList (countProjection ), false );
256
+ selectBody .setSelectItems (Collections .singletonList (new SelectExpressionItem (jSqlCount )));
257
+ return selectBody .toString ();
258
+ }
259
+
260
+ boolean distinct = selectBody .getDistinct () != null ;
261
+ selectBody .setDistinct (null ); // reset possible distinct
262
+
263
+ String tableAlias = detectAlias (selectBody );
264
+
265
+ // is never null
266
+ List <SelectItem > selectItems = selectBody .getSelectItems ();
267
+
268
+ if (onlyASingleColumnProjection (selectItems )) {
269
+ SelectExpressionItem singleProjection = (SelectExpressionItem ) selectItems .get (0 );
270
+
271
+ Column column = (Column ) singleProjection .getExpression ();
272
+ String countProp = column .getFullyQualifiedName ();
273
+
274
+ Function jSqlCount = getJSqlCount (Collections .singletonList (countProp ), distinct );
275
+ selectBody .setSelectItems (Collections .singletonList (new SelectExpressionItem (jSqlCount )));
276
+ return selectBody .toString ();
277
+ }
278
+
279
+ String countProp = tableAlias == null ? "*" : tableAlias ;
280
+
281
+ Function jSqlCount = getJSqlCount (Collections .singletonList (countProp ), distinct );
282
+ selectBody .setSelectItems (Collections .singletonList (new SelectExpressionItem (jSqlCount )));
283
+
284
+ return selectBody .toString ();
285
+
59
286
}
60
287
61
288
@ Override
62
289
public String getProjection () {
63
- return JSqlParserQueryUtils .getProjection (this .query .getQueryString ());
290
+ Assert .hasText (query .getQueryString (), "Query must not be null or empty!" );
291
+
292
+ Select selectStatement = parseSelectStatement (query .getQueryString ());
293
+ PlainSelect selectBody = (PlainSelect ) selectStatement .getSelectBody ();
294
+
295
+ return selectBody .getSelectItems () //
296
+ .stream () //
297
+ .map (Object ::toString ) //
298
+ .collect (Collectors .joining (", " )).trim ();
299
+ }
300
+
301
+ @ Override
302
+ public Set <String > getJoinAliases () {
303
+ return this .getJoinAliases (this .query .getQueryString ());
304
+ }
305
+
306
+ /**
307
+ * Parses a query string with JSqlParser.
308
+ *
309
+ * @param query the query to parse
310
+ * @return the parsed query
311
+ */
312
+ private static Select parseSelectStatement (String query ) {
313
+ try {
314
+ return (Select ) CCJSqlParserUtil .parse (query );
315
+ } catch (JSQLParserException e ) {
316
+ throw new IllegalArgumentException ("The query you provided is not a valid SQL Query!" , e );
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Checks whether a given projection only contains a single column definition (aka without functions, etc)
322
+ *
323
+ * @param projection the projection to analyse
324
+ * @return <code>true</code> when the projection only contains a single column definition otherwise <code>false</code>
325
+ */
326
+ private boolean onlyASingleColumnProjection (List <SelectItem > projection ) {
327
+ // this is unfortunately the only way to check without any hacky & hard string regex magic
328
+ return projection .size () == 1 && projection .get (0 ) instanceof SelectExpressionItem
329
+ && (((SelectExpressionItem ) projection .get (0 )).getExpression ()) instanceof Column ;
64
330
}
65
331
66
332
@ Override
0 commit comments