Skip to content

Commit 6aad414

Browse files
committed
Make JpaSort.unsafe operational.
We have offered this method as a way for people to customize an ORDER BY clause beyond a simple property name. For example, someone could use "LENGTH(firstname)" as a function to order by the length of each row's lastname instead of ordering by the lastname itself. By using the relevant parser and applying a different visitor, this commit make operational a feature that shows little evidence of ever having worked. See #3172.
1 parent 26533a1 commit 6aad414

22 files changed

+2623
-9
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository.query;
17+
18+
import jakarta.persistence.criteria.Expression;
19+
import jakarta.persistence.criteria.From;
20+
import jakarta.persistence.criteria.Path;
21+
22+
import org.antlr.v4.runtime.CharStreams;
23+
import org.antlr.v4.runtime.CommonTokenStream;
24+
import org.springframework.data.jpa.domain.JpaSort;
25+
26+
/**
27+
* Parses the content of {@link JpaSort#unsafe(String...)} as an EQL {@literal orderby_item} and renders that into a JPA
28+
* Criteria {@link Expression}.
29+
*
30+
* @author Greg Turnquist
31+
* @since 3.2
32+
*/
33+
class EqlOrderByExtractor extends EqlBaseVisitor<JpaOrderByToken> {
34+
35+
private From<?, ?> from;
36+
37+
EqlOrderByExtractor(From<?, ?> from) {
38+
this.from = from;
39+
}
40+
41+
/**
42+
* Extract the {@link JpaSort.JpaOrder}'s property and parse it as a EQL {@literal orderby_item}.
43+
*
44+
* @param jpaOrder
45+
* @return criteriaExpression
46+
* @since 3.2
47+
*/
48+
Expression<?> extractCriteriaExpression(JpaSort.JpaOrder jpaOrder) {
49+
50+
EqlLexer jpaOrderLexer = new EqlLexer(CharStreams.fromString(jpaOrder.getProperty()));
51+
EqlParser jpaOrderParser = new EqlParser(new CommonTokenStream(jpaOrderLexer));
52+
53+
return expression(visit(jpaOrderParser.orderby_item()));
54+
}
55+
56+
/**
57+
* Given a particular {@link JpaOrderByToken}, transform it into a Jakarta {@link Expression}.
58+
*
59+
* @param token
60+
* @return Expression
61+
*/
62+
private Expression<?> expression(JpaOrderByToken token) {
63+
64+
if (token instanceof JpaOrderByExpressionToken expressionToken) {
65+
return expressionToken.expression();
66+
} else if (token instanceof JpaOrderByNamedToken namedToken) {
67+
return from.get(namedToken.token());
68+
} else {
69+
if (token != null) {
70+
throw new IllegalArgumentException("We can't handle a " + token.getClass() + "!");
71+
} else {
72+
throw new IllegalArgumentException("We can't handle a null token!");
73+
}
74+
}
75+
}
76+
77+
/**
78+
* Convert a generic {@link JpaOrderByToken} token into a {@link JpaOrderByNamedToken} and then extract its string
79+
* token value.
80+
*
81+
* @param token
82+
* @return string value
83+
* @since 3.2
84+
*/
85+
private String token(JpaOrderByToken token) {
86+
87+
if (token instanceof JpaOrderByNamedToken namedToken) {
88+
return namedToken.token();
89+
} else {
90+
if (token != null) {
91+
throw new IllegalArgumentException("We can't handle a " + token.getClass() + "!");
92+
} else {
93+
throw new IllegalArgumentException("We can't handle a null token!");
94+
}
95+
}
96+
}
97+
98+
@Override
99+
public JpaOrderByToken visitOrderby_item(EqlParser.Orderby_itemContext ctx) {
100+
101+
if (ctx.state_field_path_expression() != null) {
102+
return visit(ctx.state_field_path_expression());
103+
} else if (ctx.general_identification_variable() != null) {
104+
return visit(ctx.general_identification_variable());
105+
} else if (ctx.result_variable() != null) {
106+
return visit(ctx.result_variable());
107+
} else {
108+
return null;
109+
}
110+
}
111+
112+
@Override
113+
public JpaOrderByToken visitState_field_path_expression(EqlParser.State_field_path_expressionContext ctx) {
114+
115+
Path<?> path = (Path<?>) expression(visit(ctx.general_subpath()));
116+
117+
path = path.get(token(visit(ctx.state_field())));
118+
119+
return new JpaOrderByExpressionToken(path);
120+
}
121+
122+
@Override
123+
public JpaOrderByToken visitGeneral_identification_variable(EqlParser.General_identification_variableContext ctx) {
124+
125+
if (ctx.identification_variable() != null) {
126+
return visit(ctx.identification_variable());
127+
} else {
128+
return null;
129+
}
130+
}
131+
132+
@Override
133+
public JpaOrderByToken visitSimple_subpath(EqlParser.Simple_subpathContext ctx) {
134+
135+
Path<?> path = (Path<?>) expression(visit(ctx.general_identification_variable()));
136+
137+
for (EqlParser.Single_valued_object_fieldContext singleValuedObjectFieldContext : ctx
138+
.single_valued_object_field()) {
139+
path = path.get(token(visit(singleValuedObjectFieldContext)));
140+
}
141+
142+
return new JpaOrderByExpressionToken(path);
143+
}
144+
145+
@Override
146+
public JpaOrderByToken visitResult_variable(EqlParser.Result_variableContext ctx) {
147+
return super.visitResult_variable(ctx);
148+
}
149+
150+
@Override
151+
public JpaOrderByToken visitIdentification_variable(EqlParser.Identification_variableContext ctx) {
152+
153+
if (ctx.IDENTIFICATION_VARIABLE() != null) {
154+
return new JpaOrderByNamedToken(ctx.IDENTIFICATION_VARIABLE().getText());
155+
} else {
156+
return new JpaOrderByNamedToken(ctx.f.getText());
157+
}
158+
}
159+
}

0 commit comments

Comments
 (0)