Skip to content

Commit aa6b908

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 aa6b908

22 files changed

+2631
-9
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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+
Expression<?> expression = expression(visit(jpaOrderParser.orderby_item()));
54+
55+
return expression;
56+
}
57+
58+
/**
59+
* Given a particular {@link JpaOrderByToken}, transform it into a Jakarta {@link Expression}.
60+
*
61+
* @param token
62+
* @return Expression
63+
*/
64+
private Expression<?> expression(JpaOrderByToken token) {
65+
66+
if (token instanceof JpaOrderByExpressionToken expressionToken) {
67+
return expressionToken.expression();
68+
} else if (token instanceof JpaOrderByNamedToken namedToken) {
69+
return from.get(namedToken.token());
70+
} else {
71+
if (token != null) {
72+
throw new IllegalArgumentException("We can't handle a " + token.getClass() + "!");
73+
} else {
74+
throw new IllegalArgumentException("We can't handle a null token!");
75+
}
76+
}
77+
}
78+
79+
/**
80+
* Convert a generic {@link JpaOrderByToken} token into a {@link JpaOrderByNamedToken} and then extract its string
81+
* token value.
82+
*
83+
* @param token
84+
* @return string value
85+
* @since 3.2
86+
*/
87+
private String token(JpaOrderByToken token) {
88+
89+
if (token instanceof JpaOrderByNamedToken namedToken) {
90+
return namedToken.token();
91+
} else {
92+
if (token != null) {
93+
throw new IllegalArgumentException("We can't handle a " + token.getClass() + "!");
94+
} else {
95+
throw new IllegalArgumentException("We can't handle a null token!");
96+
}
97+
}
98+
}
99+
100+
@Override
101+
public JpaOrderByToken visitOrderby_item(EqlParser.Orderby_itemContext ctx) {
102+
103+
if (ctx.state_field_path_expression() != null) {
104+
return visit(ctx.state_field_path_expression());
105+
} else if (ctx.general_identification_variable() != null) {
106+
return visit(ctx.general_identification_variable());
107+
} else if (ctx.result_variable() != null) {
108+
return visit(ctx.result_variable());
109+
} else {
110+
return null;
111+
}
112+
}
113+
114+
@Override
115+
public JpaOrderByToken visitState_field_path_expression(EqlParser.State_field_path_expressionContext ctx) {
116+
117+
Path<?> path = (Path<?>) expression(visit(ctx.general_subpath()));
118+
119+
path = path.get(token(visit(ctx.state_field())));
120+
121+
return new JpaOrderByExpressionToken(path);
122+
}
123+
124+
@Override
125+
public JpaOrderByToken visitGeneral_identification_variable(EqlParser.General_identification_variableContext ctx) {
126+
127+
if (ctx.identification_variable() != null) {
128+
return visit(ctx.identification_variable());
129+
} else {
130+
return null;
131+
}
132+
}
133+
134+
@Override
135+
public JpaOrderByToken visitSimple_subpath(EqlParser.Simple_subpathContext ctx) {
136+
137+
Path<?> path = (Path<?>) expression(visit(ctx.general_identification_variable()));
138+
139+
for (EqlParser.Single_valued_object_fieldContext singleValuedObjectFieldContext : ctx
140+
.single_valued_object_field()) {
141+
path = path.get(token(visit(singleValuedObjectFieldContext)));
142+
}
143+
144+
return new JpaOrderByExpressionToken(path);
145+
}
146+
147+
@Override
148+
public JpaOrderByToken visitResult_variable(EqlParser.Result_variableContext ctx) {
149+
return super.visitResult_variable(ctx);
150+
}
151+
152+
@Override
153+
public JpaOrderByToken visitIdentification_variable(EqlParser.Identification_variableContext ctx) {
154+
155+
if (ctx.IDENTIFICATION_VARIABLE() != null) {
156+
return new JpaOrderByNamedToken(ctx.IDENTIFICATION_VARIABLE().getText());
157+
} else {
158+
return new JpaOrderByNamedToken(ctx.f.getText());
159+
}
160+
}
161+
}

0 commit comments

Comments
 (0)