From 26533a1cf42a5f6fd4ae6ea746de337ecc47bc7e Mon Sep 17 00:00:00 2001 From: "Greg L. Turnquist" Date: Tue, 26 Sep 2023 16:15:04 -0500 Subject: [PATCH 1/2] Create branch. --- pom.xml | 2 +- spring-data-envers/pom.xml | 4 ++-- spring-data-jpa-distribution/pom.xml | 2 +- spring-data-jpa/pom.xml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 94542470ab..ee0112a079 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.2.0-SNAPSHOT + 3.2.0-gh-3172-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index f239d6394b..96a80990d7 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -5,12 +5,12 @@ org.springframework.data spring-data-envers - 3.2.0-SNAPSHOT + 3.2.0-gh-3172-SNAPSHOT org.springframework.data spring-data-jpa-parent - 3.2.0-SNAPSHOT + 3.2.0-gh-3172-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index a458953182..99370f8100 100644 --- a/spring-data-jpa-distribution/pom.xml +++ b/spring-data-jpa-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-jpa-parent - 3.2.0-SNAPSHOT + 3.2.0-gh-3172-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 2c9677329f..e8893cca43 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-jpa - 3.2.0-SNAPSHOT + 3.2.0-gh-3172-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-jpa-parent - 3.2.0-SNAPSHOT + 3.2.0-gh-3172-SNAPSHOT ../pom.xml From 6aad414f0c17fedbf58c23e6dbd28e901bcce2ee Mon Sep 17 00:00:00 2001 From: "Greg L. Turnquist" Date: Wed, 27 Sep 2023 09:08:29 -0500 Subject: [PATCH 2/2] 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. --- .../repository/query/EqlOrderByExtractor.java | 159 ++++++ .../repository/query/HqlOrderByExtractor.java | 526 ++++++++++++++++++ .../query/JpaOrderByExpressionToken.java | 27 + .../query/JpaOrderByNamedToken.java | 26 + .../jpa/repository/query/JpaOrderByToken.java | 24 + .../query/JpqlOrderByExtractor.java | 182 ++++++ .../data/jpa/repository/query/QueryUtils.java | 19 +- .../data/jpa/provider/HideEclipseLink.java | 36 ++ .../data/jpa/provider/HideHibernate.java | 36 ++ .../provider/HidePersistenceProviders.java | 41 ++ .../PersistenceProviderHiderExtension.java | 88 +++ .../provider/PersistenceProviderUtils.java | 119 ++++ ...UtilsClassLevelEclipseLinkHidingTests.java | 36 ++ ...velHibernateAndEclipseLinkHidingTests.java | 36 ++ ...erUtilsClassLevelHibernateHidingTests.java | 36 ++ .../PersistenceProviderUtilsTests.java | 111 ++++ .../jpa/repository/JpaSortUnsafeEqlTests.java | 145 +++++ .../jpa/repository/JpaSortUnsafeHqlTests.java | 381 +++++++++++++ .../JpaSortUnsafeJpqlOnEclipseLinkTests.java | 146 +++++ .../JpaSortUnsafeJpqlOnHibernateTests.java | 146 +++++ .../jpa/repository/UserRepositoryTests.java | 278 +++++++++ .../query/QueryEnhancerFactoryUnitTests.java | 34 +- 22 files changed, 2623 insertions(+), 9 deletions(-) create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlOrderByExtractor.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderByExtractor.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaOrderByExpressionToken.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaOrderByNamedToken.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaOrderByToken.java create mode 100644 spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlOrderByExtractor.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/HideEclipseLink.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/HideHibernate.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/HidePersistenceProviders.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderHiderExtension.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtils.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtilsClassLevelEclipseLinkHidingTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtilsClassLevelHibernateAndEclipseLinkHidingTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtilsClassLevelHibernateHidingTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtilsTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JpaSortUnsafeEqlTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JpaSortUnsafeHqlTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JpaSortUnsafeJpqlOnEclipseLinkTests.java create mode 100644 spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JpaSortUnsafeJpqlOnHibernateTests.java diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlOrderByExtractor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlOrderByExtractor.java new file mode 100644 index 0000000000..18672a10f4 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlOrderByExtractor.java @@ -0,0 +1,159 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.From; +import jakarta.persistence.criteria.Path; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.springframework.data.jpa.domain.JpaSort; + +/** + * Parses the content of {@link JpaSort#unsafe(String...)} as an EQL {@literal orderby_item} and renders that into a JPA + * Criteria {@link Expression}. + * + * @author Greg Turnquist + * @since 3.2 + */ +class EqlOrderByExtractor extends EqlBaseVisitor { + + private From from; + + EqlOrderByExtractor(From from) { + this.from = from; + } + + /** + * Extract the {@link JpaSort.JpaOrder}'s property and parse it as a EQL {@literal orderby_item}. + * + * @param jpaOrder + * @return criteriaExpression + * @since 3.2 + */ + Expression extractCriteriaExpression(JpaSort.JpaOrder jpaOrder) { + + EqlLexer jpaOrderLexer = new EqlLexer(CharStreams.fromString(jpaOrder.getProperty())); + EqlParser jpaOrderParser = new EqlParser(new CommonTokenStream(jpaOrderLexer)); + + return expression(visit(jpaOrderParser.orderby_item())); + } + + /** + * Given a particular {@link JpaOrderByToken}, transform it into a Jakarta {@link Expression}. + * + * @param token + * @return Expression + */ + private Expression expression(JpaOrderByToken token) { + + if (token instanceof JpaOrderByExpressionToken expressionToken) { + return expressionToken.expression(); + } else if (token instanceof JpaOrderByNamedToken namedToken) { + return from.get(namedToken.token()); + } else { + if (token != null) { + throw new IllegalArgumentException("We can't handle a " + token.getClass() + "!"); + } else { + throw new IllegalArgumentException("We can't handle a null token!"); + } + } + } + + /** + * Convert a generic {@link JpaOrderByToken} token into a {@link JpaOrderByNamedToken} and then extract its string + * token value. + * + * @param token + * @return string value + * @since 3.2 + */ + private String token(JpaOrderByToken token) { + + if (token instanceof JpaOrderByNamedToken namedToken) { + return namedToken.token(); + } else { + if (token != null) { + throw new IllegalArgumentException("We can't handle a " + token.getClass() + "!"); + } else { + throw new IllegalArgumentException("We can't handle a null token!"); + } + } + } + + @Override + public JpaOrderByToken visitOrderby_item(EqlParser.Orderby_itemContext ctx) { + + if (ctx.state_field_path_expression() != null) { + return visit(ctx.state_field_path_expression()); + } else if (ctx.general_identification_variable() != null) { + return visit(ctx.general_identification_variable()); + } else if (ctx.result_variable() != null) { + return visit(ctx.result_variable()); + } else { + return null; + } + } + + @Override + public JpaOrderByToken visitState_field_path_expression(EqlParser.State_field_path_expressionContext ctx) { + + Path path = (Path) expression(visit(ctx.general_subpath())); + + path = path.get(token(visit(ctx.state_field()))); + + return new JpaOrderByExpressionToken(path); + } + + @Override + public JpaOrderByToken visitGeneral_identification_variable(EqlParser.General_identification_variableContext ctx) { + + if (ctx.identification_variable() != null) { + return visit(ctx.identification_variable()); + } else { + return null; + } + } + + @Override + public JpaOrderByToken visitSimple_subpath(EqlParser.Simple_subpathContext ctx) { + + Path path = (Path) expression(visit(ctx.general_identification_variable())); + + for (EqlParser.Single_valued_object_fieldContext singleValuedObjectFieldContext : ctx + .single_valued_object_field()) { + path = path.get(token(visit(singleValuedObjectFieldContext))); + } + + return new JpaOrderByExpressionToken(path); + } + + @Override + public JpaOrderByToken visitResult_variable(EqlParser.Result_variableContext ctx) { + return super.visitResult_variable(ctx); + } + + @Override + public JpaOrderByToken visitIdentification_variable(EqlParser.Identification_variableContext ctx) { + + if (ctx.IDENTIFICATION_VARIABLE() != null) { + return new JpaOrderByNamedToken(ctx.IDENTIFICATION_VARIABLE().getText()); + } else { + return new JpaOrderByNamedToken(ctx.f.getText()); + } + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderByExtractor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderByExtractor.java new file mode 100644 index 0000000000..f5623c6d34 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlOrderByExtractor.java @@ -0,0 +1,526 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.From; +import jakarta.persistence.criteria.Path; + +import java.util.HexFormat; +import java.util.stream.Collectors; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.springframework.data.jpa.domain.JpaSort; + +/** + * Parses the content of {@link JpaSort#unsafe(String...)} as an HQL {@literal sortExpression} and renders that into a + * JPA Criteria {@link Expression}. + * + * @author Greg Turnquist + * @since 3.2 + */ +class HqlOrderByExtractor extends HqlBaseVisitor { + + private CriteriaBuilder cb; + private From from; + + private static String UNSUPPORTED_TEMPLATE = "We can't handle %s in an ORDER BY clause through JpaSort.unsafe"; + + HqlOrderByExtractor(CriteriaBuilder cb, From from) { + + this.cb = cb; + this.from = from; + } + + /** + * Extract the {@link org.springframework.data.jpa.domain.JpaSort.JpaOrder}'s property and parse it as an HQL + * {@literal sortExpression}. + * + * @param jpaOrder + * @return criteriaExpression + * @since 3.2 + */ + Expression extractCriteriaExpression(JpaSort.JpaOrder jpaOrder) { + + HqlLexer jpaOrderLexer = new HqlLexer(CharStreams.fromString(jpaOrder.getProperty())); + HqlParser jpaOrderParser = new HqlParser(new CommonTokenStream(jpaOrderLexer)); + + return expression(visit(jpaOrderParser.sortExpression())); + } + + /** + * Given a particular {@link JpaOrderByToken}, transform it into a Jakarta {@link Expression}. + * + * @param token + * @return Expression + */ + private Expression expression(JpaOrderByToken token) { + + if (token instanceof JpaOrderByExpressionToken expressionToken) { + return expressionToken.expression(); + } else if (token instanceof JpaOrderByNamedToken namedToken) { + return from.get(namedToken.token()); + } else { + throw new IllegalArgumentException("We can't handle a " + token.getClass() + "!"); + } + } + + /** + * Convert a generic {@link JpaOrderByToken} token into a {@link JpaOrderByNamedToken} and then extract its string + * token value. + * + * @param token + * @return string value + * @since 3.2 + */ + private String token(JpaOrderByToken token) { + + if (token instanceof JpaOrderByNamedToken namedToken) { + return namedToken.token(); + } else { + throw new IllegalArgumentException("We can't handle a " + token.getClass() + "!"); + } + } + + @Override + public JpaOrderByToken visitSortExpression(HqlParser.SortExpressionContext ctx) { + + if (ctx.identifier() != null) { + return visit(ctx.identifier()); + } else if (ctx.INTEGER_LITERAL() != null) { + return new JpaOrderByExpressionToken(cb.literal(Integer.valueOf(ctx.INTEGER_LITERAL().getText()))); + } else if (ctx.expression() != null) { + return visit(ctx.expression()); + } else { + return null; + } + } + + @Override + public JpaOrderByToken visitRelationalExpression(HqlParser.RelationalExpressionContext ctx) { + + Expression left = (Expression) expression(visit(ctx.expression(0))); + Expression right = (Expression) expression(visit(ctx.expression(1))); + + if (ctx.op.getText().equals("=")) { + return new JpaOrderByExpressionToken(cb.equal(left, right)); + } else if (ctx.op.getText().equals(">")) { + return new JpaOrderByExpressionToken(cb.greaterThan(left, right)); + } else if (ctx.op.getText().equals(">=")) { + return new JpaOrderByExpressionToken(cb.greaterThanOrEqualTo(left, right)); + } else if (ctx.op.getText().equals("<")) { + return new JpaOrderByExpressionToken(cb.lessThan(left, right)); + } else if (ctx.op.getText().equals("<=")) { + return new JpaOrderByExpressionToken(cb.lessThanOrEqualTo(left, right)); + } else if (ctx.op.getText().equals("<>") || ctx.op.getText().equals("!=") || ctx.op.getText().equals("^=")) { + return new JpaOrderByExpressionToken(cb.notEqual(left, right)); + } else { + return null; + } + } + + @Override + public JpaOrderByToken visitBetweenExpression(HqlParser.BetweenExpressionContext ctx) { + + Expression condition = (Expression) expression(visit(ctx.expression(0))); + Expression lower = (Expression) expression(visit(ctx.expression(1))); + Expression upper = (Expression) expression(visit(ctx.expression(2))); + + if (ctx.NOT() == null) { + return new JpaOrderByExpressionToken(cb.between(condition, lower, upper)); + } else { + return new JpaOrderByExpressionToken(cb.between(condition, lower, upper).not()); + } + } + + @Override + public JpaOrderByToken visitDealingWithNullExpression(HqlParser.DealingWithNullExpressionContext ctx) { + + if (ctx.NULL() != null) { + + Expression condition = expression(visit(ctx.expression(0))); + + if (ctx.NOT() == null) { + return new JpaOrderByExpressionToken(cb.isNull(condition)); + } else { + return new JpaOrderByExpressionToken(cb.isNotNull(condition)); + } + } else { + + Expression left = expression(visit(ctx.expression(0))); + Expression right = expression(visit(ctx.expression(1))); + + HibernateCriteriaBuilder hcb = (HibernateCriteriaBuilder) cb; + + if (ctx.NOT() == null) { + return new JpaOrderByExpressionToken(hcb.distinctFrom(left, right)); + } else { + return new JpaOrderByExpressionToken(hcb.notDistinctFrom(left, right)); + } + } + } + + @Override + public JpaOrderByToken visitStringPatternMatching(HqlParser.StringPatternMatchingContext ctx) { + + Expression condition = (Expression) expression(visit(ctx.expression(0))); + Expression match = (Expression) expression(visit(ctx.expression(1))); + Expression escape = ctx.ESCAPE() != null && ctx.stringLiteral() != null // + ? (Expression) expression(visit(ctx.stringLiteral())) // + : null; + + if (ctx.LIKE() != null) { + + if (ctx.NOT() == null) { + return escape == null // + ? new JpaOrderByExpressionToken(cb.like(condition, match)) // + : new JpaOrderByExpressionToken(cb.like(condition, match, escape)); + } else { + return escape == null // + ? new JpaOrderByExpressionToken(cb.notLike(condition, match)) // + : new JpaOrderByExpressionToken(cb.notLike(condition, match, escape)); + } + } else { + + HibernateCriteriaBuilder hcb = (HibernateCriteriaBuilder) cb; + + if (ctx.NOT() == null) { + return escape == null // + ? new JpaOrderByExpressionToken(hcb.ilike(condition, match)) // + : new JpaOrderByExpressionToken(hcb.ilike(condition, match, escape)); + } else { + return escape == null // + ? new JpaOrderByExpressionToken(hcb.notIlike(condition, match)) // + : new JpaOrderByExpressionToken(hcb.notIlike(condition, match, escape)); + } + } + + } + + @Override + public JpaOrderByToken visitInExpression(HqlParser.InExpressionContext ctx) { + + if (ctx.inList().simplePath() != null) { + throw new UnsupportedOperationException( + String.format(UNSUPPORTED_TEMPLATE, "IN clause with ELEMENTS or INDICES argument")); + } else if (ctx.inList().subquery() != null) { + throw new UnsupportedOperationException(String.format(UNSUPPORTED_TEMPLATE, "IN clause with a subquery")); + } else if (ctx.inList().parameter() != null) { + throw new UnsupportedOperationException(String.format(UNSUPPORTED_TEMPLATE, "IN clause with a parameter")); + } + + CriteriaBuilder.In in = cb.in(expression(visit(ctx.expression()))); + + ctx.inList().expressionOrPredicate() + .forEach(expressionOrPredicateContext -> in.value(expression(visit(expressionOrPredicateContext)))); + + if (ctx.NOT() == null) { + return new JpaOrderByExpressionToken(in); + } else { + return new JpaOrderByExpressionToken(in.not()); + } + } + + @Override + public JpaOrderByToken visitGenericFunction(HqlParser.GenericFunctionContext ctx) { + + String functionName = token(visit(ctx.functionName())); + + if (ctx.functionArguments() == null) { + return new JpaOrderByExpressionToken(cb.function(functionName, Object.class)); + } else { + + Expression[] arguments = ctx.functionArguments().expressionOrPredicate().stream() // + .map(expressionOrPredicateContext -> expression(visit(expressionOrPredicateContext))) // + .toArray(Expression[]::new); + + return new JpaOrderByExpressionToken(cb.function(functionName, Object.class, arguments)); + } + } + + @Override + public JpaOrderByToken visitFunctionName(HqlParser.FunctionNameContext ctx) { + + String functionName = ctx.reservedWord().stream() // + .map(reservedWordContext -> token(visit(reservedWordContext))) // + .collect(Collectors.joining(".")); + + return new JpaOrderByNamedToken(functionName); + } + + @Override + public JpaOrderByToken visitTrimFunction(HqlParser.TrimFunctionContext ctx) { + + CriteriaBuilder.Trimspec trimSpec = null; + + if (ctx.LEADING() != null) { + trimSpec = CriteriaBuilder.Trimspec.LEADING; + } else if (ctx.TRAILING() != null) { + trimSpec = CriteriaBuilder.Trimspec.TRAILING; + } else if (ctx.BOTH() != null) { + trimSpec = CriteriaBuilder.Trimspec.BOTH; + } + + Expression stringLiteral = ctx.stringLiteral() != null // + ? expression(visit(ctx.stringLiteral())) // + : null; + + Expression expression = expression(visit(ctx.expression())); + + if (trimSpec != null) { + return stringLiteral != null // + ? new JpaOrderByExpressionToken( + cb.trim(trimSpec, (Expression) stringLiteral, (Expression) expression)) // + : new JpaOrderByExpressionToken(cb.trim(trimSpec, (Expression) expression)); + } else { + return stringLiteral != null // + ? new JpaOrderByExpressionToken( + cb.trim((Expression) stringLiteral, (Expression) expression)) // + : new JpaOrderByExpressionToken(cb.trim((Expression) expression)); + } + } + + @Override + public JpaOrderByToken visitLiteral(HqlParser.LiteralContext ctx) { + + if (ctx.NULL() != null) { + return new JpaOrderByNamedToken(ctx.getText()); + } else if (ctx.booleanLiteral() != null) { + return visit(ctx.booleanLiteral()); + } else if (ctx.stringLiteral() != null) { + return visit(ctx.stringLiteral()); + } else if (ctx.numericLiteral() != null) { + return visit(ctx.numericLiteral()); + } else if (ctx.dateTimeLiteral() != null) { + return visit(ctx.dateTimeLiteral()); + } else if (ctx.binaryLiteral() != null) { + return visit(ctx.binaryLiteral()); + } else { + return null; + } + } + + @Override + public JpaOrderByToken visitBooleanLiteral(HqlParser.BooleanLiteralContext ctx) { + + if (ctx.TRUE() != null) { + return new JpaOrderByExpressionToken(cb.literal(true)); + } else { + return new JpaOrderByExpressionToken(cb.literal(false)); + } + } + + @Override + public JpaOrderByToken visitStringLiteral(HqlParser.StringLiteralContext ctx) { + + if (ctx.STRINGLITERAL() != null) { + + String literal = ctx.STRINGLITERAL().getText(); + return new JpaOrderByExpressionToken(cb.literal(literal.substring(1, literal.length() - 1))); + + } else if (ctx.JAVASTRINGLITERAL() != null) { + + String literal = ctx.JAVASTRINGLITERAL().getText(); + return new JpaOrderByExpressionToken(cb.literal(literal.substring(1, literal.length() - 1))); + + } else if (ctx.CHARACTER() != null) { + // Skip over the single quote + return new JpaOrderByExpressionToken(cb.literal(ctx.CHARACTER().getText().charAt(1))); + } else { + return null; + } + } + + @Override + public JpaOrderByToken visitNumericLiteral(HqlParser.NumericLiteralContext ctx) { + + if (ctx.INTEGER_LITERAL() != null) { + return new JpaOrderByExpressionToken(cb.literal(Integer.valueOf(ctx.INTEGER_LITERAL().getText()))); + } else if (ctx.FLOAT_LITERAL() != null) { + return new JpaOrderByExpressionToken(cb.literal(Float.valueOf(ctx.FLOAT_LITERAL().getText()))); + } else if (ctx.HEXLITERAL() != null) { + return new JpaOrderByExpressionToken(cb.literal(HexFormat.fromHexDigits(ctx.HEXLITERAL().toString()))); + } else { + return null; + } + } + + @Override + public JpaOrderByToken visitDateTimeLiteral(HqlParser.DateTimeLiteralContext ctx) { + + if (ctx.LOCAL_DATE() != null) { + return new JpaOrderByNamedToken(ctx.LOCAL_DATE().getText()); + } else if (ctx.LOCAL_TIME() != null) { + return new JpaOrderByNamedToken(ctx.LOCAL_TIME().getText()); + } else if (ctx.LOCAL_DATETIME() != null) { + return new JpaOrderByNamedToken(ctx.LOCAL_DATETIME().getText()); + } else if (ctx.CURRENT_DATE() != null) { + return new JpaOrderByNamedToken(ctx.CURRENT_DATE().getText()); + } else if (ctx.CURRENT_TIME() != null) { + return new JpaOrderByNamedToken(ctx.CURRENT_TIME().getText()); + } else if (ctx.CURRENT_TIMESTAMP() != null) { + return new JpaOrderByNamedToken(ctx.CURRENT_TIMESTAMP().getText()); + } + + if (ctx.DATE() != null) { + if (ctx.LOCAL() != null) { + return new JpaOrderByNamedToken(ctx.LOCAL().getText() + " " + ctx.DATE().getText()); + } else { + return new JpaOrderByNamedToken(ctx.CURRENT().getText() + " " + ctx.TIME().getText()); + } + } + + if (ctx.DATETIME() != null) { + if (ctx.LOCAL() != null) { + return new JpaOrderByNamedToken(ctx.LOCAL().getText() + " " + ctx.DATETIME().getText()); + } else if (ctx.CURRENT() != null) { + return new JpaOrderByNamedToken(ctx.CURRENT().getText() + " " + ctx.DATETIME().getText()); + } else { + return new JpaOrderByNamedToken(ctx.OFFSET().getText() + " " + ctx.DATETIME().getText()); + + } + } + + return new JpaOrderByNamedToken(ctx.INSTANT().getText()); + } + + @Override + public JpaOrderByToken visitGroupedExpression(HqlParser.GroupedExpressionContext ctx) { + return visit(ctx.expression()); + } + + @Override + public JpaOrderByToken visitTupleExpression(HqlParser.TupleExpressionContext ctx) { + throw new UnsupportedOperationException(String.format(UNSUPPORTED_TEMPLATE, "a TUPLE argument")); + } + + @Override + public JpaOrderByToken visitSubqueryExpression(HqlParser.SubqueryExpressionContext ctx) { + throw new UnsupportedOperationException(String.format(UNSUPPORTED_TEMPLATE, "a subquery argument")); + } + + @Override + public JpaOrderByToken visitMultiplicationExpression(HqlParser.MultiplicationExpressionContext ctx) { + + Expression left = (Expression) expression(visit(ctx.expression(0))); + Expression right = (Expression) expression(visit(ctx.expression(1))); + + if (ctx.op.getText().equals("*")) { + return new JpaOrderByExpressionToken(cb.prod(left, right)); + } else { + return new JpaOrderByExpressionToken(cb.quot(left, right)); + } + } + + @Override + public JpaOrderByToken visitAdditionExpression(HqlParser.AdditionExpressionContext ctx) { + + Expression left = (Expression) expression(visit(ctx.expression(0))); + Expression right = (Expression) expression(visit(ctx.expression(1))); + + if (ctx.op.getText().equals("+")) { + return new JpaOrderByExpressionToken(cb.sum(left, right)); + } else { + return new JpaOrderByExpressionToken(cb.diff(left, right)); + } + } + + @Override + public JpaOrderByToken visitHqlConcatenationExpression(HqlParser.HqlConcatenationExpressionContext ctx) { + + Expression left = (Expression) expression(visit(ctx.expression(0))); + Expression right = (Expression) expression(visit(ctx.expression(1))); + + return new JpaOrderByExpressionToken(cb.concat(left, right)); + } + + @Override + public JpaOrderByToken visitSimplePath(HqlParser.SimplePathContext ctx) { + + Path simplePath = (Path) expression(visit(ctx.identifier())); + + for (HqlParser.SimplePathElementContext simplePathElementContext : ctx.simplePathElement()) { + simplePath = simplePath.get(token(visit(simplePathElementContext))); + } + + return new JpaOrderByExpressionToken(simplePath); + } + + @Override + public JpaOrderByToken visitCaseList(HqlParser.CaseListContext ctx) { + + if (ctx.simpleCaseExpression() != null) { + return visit(ctx.simpleCaseExpression()); + } else { + return visit(ctx.searchedCaseExpression()); + } + } + + @Override + public JpaOrderByToken visitSimpleCaseExpression(HqlParser.SimpleCaseExpressionContext ctx) { + + CriteriaBuilder.SimpleCase simpleCase = cb + .selectCase(expression(visit(ctx.expressionOrPredicate(0)))); + + ctx.caseWhenExpressionClause().forEach(caseWhenExpressionClauseContext -> { + simpleCase.when( // + expression(visit(caseWhenExpressionClauseContext.expression())), // + expression(visit(caseWhenExpressionClauseContext.expressionOrPredicate()))); + }); + + if (ctx.expressionOrPredicate().size() == 2) { + simpleCase.otherwise(expression(visit(ctx.expressionOrPredicate(1)))); + } + + return new JpaOrderByExpressionToken(simpleCase); + } + + @Override + public JpaOrderByToken visitSearchedCaseExpression(HqlParser.SearchedCaseExpressionContext ctx) { + + CriteriaBuilder.Case searchedCase = cb.selectCase(); + + ctx.caseWhenPredicateClause().forEach(caseWhenPredicateClauseContext -> { + searchedCase.when( // + (Expression) expression(visit(caseWhenPredicateClauseContext.predicate())), // + expression(visit(caseWhenPredicateClauseContext.expressionOrPredicate()))); + }); + + if (ctx.expressionOrPredicate() != null) { + searchedCase.otherwise(expression(visit(ctx.expressionOrPredicate()))); + } + + return new JpaOrderByExpressionToken(searchedCase); + } + + @Override + public JpaOrderByToken visitParameter(HqlParser.ParameterContext ctx) { + throw new UnsupportedOperationException(String.format(UNSUPPORTED_TEMPLATE, "a parameter argument")); + } + + @Override + public JpaOrderByToken visitReservedWord(HqlParser.ReservedWordContext ctx) { + + if (ctx.IDENTIFICATION_VARIABLE() != null) { + return new JpaOrderByNamedToken(ctx.IDENTIFICATION_VARIABLE().getText()); + } else { + return new JpaOrderByNamedToken(ctx.f.getText()); + } + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaOrderByExpressionToken.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaOrderByExpressionToken.java new file mode 100644 index 0000000000..ee9d272cc8 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaOrderByExpressionToken.java @@ -0,0 +1,27 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import jakarta.persistence.criteria.Expression; + +/** + * A token that encloses a JPA Criteria {@link Expression}-based token, e.g. "manager.firstname". + * + * @author Greg Turnquist + * @since 3.2 + */ +record JpaOrderByExpressionToken(Expression expression) implements JpaOrderByToken { +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaOrderByNamedToken.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaOrderByNamedToken.java new file mode 100644 index 0000000000..fc3638c665 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaOrderByNamedToken.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +/** + * A token that encloses a string-based token name, e.g. "firstname" used to build a JPA Criteria + * {@link jakarta.persistence.criteria.From}. + * + * @author Greg Turnquist + * @since 3.2 + */ +record JpaOrderByNamedToken(String token) implements JpaOrderByToken { +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaOrderByToken.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaOrderByToken.java new file mode 100644 index 0000000000..d3b80b6b51 --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaOrderByToken.java @@ -0,0 +1,24 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +/** + * Base return type of an ANTLR visitor used to traverse an {@literal ORDER BY} clause. + * + * @author Greg Turnquist + * @since 3.2 + */ +interface JpaOrderByToken {} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlOrderByExtractor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlOrderByExtractor.java new file mode 100644 index 0000000000..49206c8f4f --- /dev/null +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlOrderByExtractor.java @@ -0,0 +1,182 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.From; +import jakarta.persistence.criteria.Path; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.springframework.data.jpa.domain.JpaSort; + +/** + * Parses the content of {@link JpaSort#unsafe(String...)} as a JPQL {@literal orderby_item} and renders that into a JPA + * Criteria {@link Expression}. + * + * @author Greg Turnquist + * @since 3.2 + */ +class JpqlOrderByExtractor extends JpqlBaseVisitor { + + private From from; + + JpqlOrderByExtractor(From from) { + this.from = from; + } + + /** + * Extract the {@link org.springframework.data.jpa.domain.JpaSort.JpaOrder}'s property and parse it as a JPQL + * {@literal orderby_item}. + * + * @param jpaOrder + * @return criteriaExpression + * @since 3.2 + */ + Expression extractCriteriaExpression(JpaSort.JpaOrder jpaOrder) { + + JpqlLexer jpaOrderLexer = new JpqlLexer(CharStreams.fromString(jpaOrder.getProperty())); + JpqlParser jpaOrderParser = new JpqlParser(new CommonTokenStream(jpaOrderLexer)); + + return expression(visit(jpaOrderParser.orderby_item())); + } + + /** + * Base token return type of the ANTLR visitor used to traverse the {@literal sortExpression}. + */ + interface JpqlOrderByItem {} + + /** + * A token that encloses an {@link Expression}-based token, e.g. "LENGTH(firstname)". + * + * @param expression + */ + private record JpqlOrderByItemExpressionToken(Expression expression) implements JpqlOrderByItem { + } + + /** + * A token that encloses a string-based token name, e.g. "firstname", that is usually turned into + * {@literal from.get(token)}. + * + * @param token + */ + private record JpqlOrderByItemNamedToken(String token) implements JpqlOrderByItem { + } + + /** + * Given a particular {@link JpqlOrderByItem}, transform it into a Jakarta {@link Expression}. + * + * @param token + * @return Expression + */ + private Expression expression(JpqlOrderByItem token) { + + if (token instanceof JpqlOrderByItemExpressionToken expressionToken) { + return expressionToken.expression(); + } else if (token instanceof JpqlOrderByItemNamedToken namedToken) { + return from.get(namedToken.token()); + } else { + if (token != null) { + throw new IllegalArgumentException("We can't handle a " + token.getClass() + "!"); + } else { + throw new IllegalArgumentException("We can't handle a null token!"); + } + } + } + + /** + * Convert a generic {@link JpqlOrderByItem} token into a {@link JpqlOrderByItemNamedToken} and then extract its + * string token value. + * + * @param token + * @return string value + * @since 3.2 + */ + private String token(JpqlOrderByItem token) { + + if (token instanceof JpqlOrderByItemNamedToken namedToken) { + return namedToken.token(); + } else { + if (token != null) { + throw new IllegalArgumentException("We can't handle a " + token.getClass() + "!"); + } else { + throw new IllegalArgumentException("We can't handle a null token!"); + } + } + } + + @Override + public JpqlOrderByItem visitOrderby_item(JpqlParser.Orderby_itemContext ctx) { + + if (ctx.state_field_path_expression() != null) { + return visit(ctx.state_field_path_expression()); + } else if (ctx.general_identification_variable() != null) { + return visit(ctx.general_identification_variable()); + } else if (ctx.result_variable() != null) { + return visit(ctx.result_variable()); + } else { + return null; + } + } + + @Override + public JpqlOrderByItem visitState_field_path_expression(JpqlParser.State_field_path_expressionContext ctx) { + + Path path = (Path) expression(visit(ctx.general_subpath())); + + path = path.get(token(visit(ctx.state_field()))); + + return new JpqlOrderByItemExpressionToken(path); + } + + @Override + public JpqlOrderByItem visitGeneral_identification_variable(JpqlParser.General_identification_variableContext ctx) { + + if (ctx.identification_variable() != null) { + return visit(ctx.identification_variable()); + } else { + return null; + } + } + + @Override + public JpqlOrderByItem visitSimple_subpath(JpqlParser.Simple_subpathContext ctx) { + + Path path = (Path) expression(visit(ctx.general_identification_variable())); + + for (JpqlParser.Single_valued_object_fieldContext singleValuedObjectFieldContext : ctx + .single_valued_object_field()) { + path = path.get(token(visit(singleValuedObjectFieldContext))); + } + + return new JpqlOrderByItemExpressionToken(path); + } + + @Override + public JpqlOrderByItem visitResult_variable(JpqlParser.Result_variableContext ctx) { + return super.visitResult_variable(ctx); + } + + @Override + public JpqlOrderByItem visitIdentification_variable(JpqlParser.Identification_variableContext ctx) { + + if (ctx.IDENTIFICATION_VARIABLE() != null) { + return new JpqlOrderByItemNamedToken(ctx.IDENTIFICATION_VARIABLE().getText()); + } else { + return new JpqlOrderByItemNamedToken(ctx.f.getText()); + } + } +} diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java index 021e375807..55b925ab3a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java @@ -49,6 +49,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.jpa.domain.JpaSort.JpaOrder; +import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.util.Streamable; import org.springframework.lang.Nullable; @@ -737,8 +738,22 @@ public static String getProjection(String query) { @SuppressWarnings("unchecked") private static jakarta.persistence.criteria.Order toJpaOrder(Order order, From from, CriteriaBuilder cb) { - PropertyPath property = PropertyPath.from(order.getProperty(), from.getJavaType()); - Expression expression = toExpressionRecursively(from, property); + Expression expression; + + if (order instanceof JpaOrder jpaOrder && jpaOrder.isUnsafe()) { + + if (PersistenceProvider.HIBERNATE.isPresent()) { + expression = new HqlOrderByExtractor(cb, from).extractCriteriaExpression(jpaOrder); + } else if (PersistenceProvider.ECLIPSELINK.isPresent()) { + expression = new EqlOrderByExtractor(from).extractCriteriaExpression(jpaOrder); + } else { + expression = new JpqlOrderByExtractor(from).extractCriteriaExpression(jpaOrder); + } + } else { + + PropertyPath property = PropertyPath.from(order.getProperty(), from.getJavaType()); + expression = toExpressionRecursively(from, property); + } if (order.isIgnoreCase() && String.class.equals(expression.getJavaType())) { Expression upper = cb.lower((Expression) expression); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/HideEclipseLink.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/HideEclipseLink.java new file mode 100644 index 0000000000..e890a9a22c --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/HideEclipseLink.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.provider; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Annotation to hide {@link PersistenceProvider#ECLIPSELINK} from all classpath checks during tests. + * + * @author Greg Turnquist + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@ExtendWith(PersistenceProviderHiderExtension.class) +public @interface HideEclipseLink { +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/HideHibernate.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/HideHibernate.java new file mode 100644 index 0000000000..87db9ea6ce --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/HideHibernate.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.provider; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Annotation to hide {@link PersistenceProvider#HIBERNATE} from all classpath checks during tests. + * + * @author Greg Turnquist + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@ExtendWith(PersistenceProviderHiderExtension.class) +public @interface HideHibernate { +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/HidePersistenceProviders.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/HidePersistenceProviders.java new file mode 100644 index 0000000000..01e12fb0b8 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/HidePersistenceProviders.java @@ -0,0 +1,41 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.provider; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Annotation to hide any combination of {@link PersistenceProvider}s from classpath checks for tests. + * + * @author Greg Turnquist + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@ExtendWith(PersistenceProviderHiderExtension.class) +public @interface HidePersistenceProviders { + + /** + * One or more {@link PersistenceProvider}s to hide from classpath checks. + */ + PersistenceProvider[] value(); +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderHiderExtension.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderHiderExtension.java new file mode 100644 index 0000000000..91d1adee1a --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderHiderExtension.java @@ -0,0 +1,88 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.provider; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.InvocationInterceptor; +import org.junit.jupiter.api.extension.ReflectiveInvocationContext; +import org.springframework.core.annotation.MergedAnnotations; + +/** + * A JUnit 5 extension to handle hiding certain {@link PersistenceProvider}s. + * + * @author Greg Turnquist + */ +class PersistenceProviderHiderExtension implements InvocationInterceptor { + + @Override + public void interceptTestMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, + ExtensionContext extensionContext) throws Throwable { + + Method testMethod = invocationContext.getExecutable(); + Class testClass = extensionContext.getRequiredTestClass(); + + Set candidates = Set.of(testMethod, testClass); + + Optional> persistenceProviders = candidates.stream() // + .filter(annotatedElement -> { + MergedAnnotations annotations = MergedAnnotations.from(annotatedElement, + MergedAnnotations.SearchStrategy.TYPE_HIERARCHY); + + return annotations.isPresent(HidePersistenceProviders.class) || annotations.isPresent(HideHibernate.class) + || annotations.isPresent(HideEclipseLink.class); + }) // + .findFirst() // + .map(source -> { + + MergedAnnotations annotations = MergedAnnotations.from(source, + MergedAnnotations.SearchStrategy.TYPE_HIERARCHY); + + if (annotations.get(HideHibernate.class).isPresent()) { + return List.of(PersistenceProvider.HIBERNATE); + } + + if (annotations.get(HideEclipseLink.class).isPresent()) { + return List.of(PersistenceProvider.ECLIPSELINK); + } + + if (annotations.isPresent(HidePersistenceProviders.class)) { + return List.of( // + annotations.get(HidePersistenceProviders.class).getEnumArray("value", PersistenceProvider.class)); + } + + return List.of(); + }); + + Runnable testMethodInvocation = () -> { + try { + invocation.proceed(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + }; + + persistenceProviders.ifPresentOrElse( + providers -> PersistenceProviderUtils.doWithPersistenceProvidersHidden(providers, testMethodInvocation), + testMethodInvocation); + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtils.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtils.java new file mode 100644 index 0000000000..0181ecdf05 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtils.java @@ -0,0 +1,119 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.provider; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.util.ReflectionUtils; + +/** + * Utility to hide various {@link PersistenceProvider} from test scenarios. + * + * @author Greg Turnquist + */ +public final class PersistenceProviderUtils { + + private PersistenceProviderUtils() { + // Utility class + } + + public static void doWithHibernateHidden(Runnable callback) { + doWithPersistenceProvidersHidden(List.of(PersistenceProvider.HIBERNATE), callback); + } + + public static void doWithEclipseLinkHidden(Runnable callback) { + doWithPersistenceProvidersHidden(List.of(PersistenceProvider.ECLIPSELINK), callback); + } + + public static void doWithPersistenceProvidersHidden(List persistenceProviders, + Runnable callback) { + + try (PersistenceProviderHider hidden = new PersistenceProviderHider(persistenceProviders)) { + + hidden.hide(); + callback.run(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Utility class that will hide a list of {@link PersistenceProvider}s visible status, thus altering classpath checks, + * and then later restores that status once the try-with-resources block is exited. + * + * @author Greg Turnquist + */ + private static class PersistenceProviderHider implements AutoCloseable { + + private static Field presentField; + private Map persistenceProviderCache; + + /** + * Make the {@link PersistenceProvider#isPresent()}} field accessible so it can be altered temporarily for test + * purposes. + */ + static { + presentField = ReflectionUtils.findField(PersistenceProvider.class, "present"); + ReflectionUtils.makeAccessible(presentField); + } + + /** + * Initialize the cache with {@literal null} values for every {@link PersistenceProvider}. + */ + public PersistenceProviderHider(List persistenceProviders) { + + this.persistenceProviderCache = new HashMap<>(); + + persistenceProviders.forEach(persistenceProvider -> { + this.persistenceProviderCache.put(persistenceProvider, null); + }); + } + + /** + * Cache the current state of the list of {@link PersistenceProvider}s and then set their visible status to + * {@literal false}, thus hiding them from classpath checks. + */ + public void hide() { + + persistenceProviderCache.keySet().forEach(persistenceProvider -> { + + persistenceProviderCache.put(persistenceProvider, persistenceProvider.isPresent()); + ReflectionUtils.setField(presentField, persistenceProvider, false); + }); + } + + /** + * Update the list of {@link PersistenceProvider}s visible status to their cached value and then null out their + * cached status, thus making them available to classpath checks. + */ + public void restore() { + + persistenceProviderCache.keySet().forEach(persistenceProvider -> { + + ReflectionUtils.setField(presentField, persistenceProvider, persistenceProviderCache.get(persistenceProvider)); + persistenceProviderCache.put(persistenceProvider, null); + }); + } + + @Override + public void close() { + restore(); + } + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtilsClassLevelEclipseLinkHidingTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtilsClassLevelEclipseLinkHidingTests.java new file mode 100644 index 0000000000..54cecd6896 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtilsClassLevelEclipseLinkHidingTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.provider; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * Test cases to verify {@link PersistenceProviderUtils} annotations work at the class level. + * + * @author Greg Turnquist + */ +@HidePersistenceProviders(PersistenceProvider.ECLIPSELINK) +class PersistenceProviderUtilsClassLevelEclipseLinkHidingTests { + + @Test + void hidingHibernateAtTheClassLevelShouldWork() { + + assertThat(PersistenceProvider.HIBERNATE.isPresent()).isTrue(); + assertThat(PersistenceProvider.ECLIPSELINK.isPresent()).isFalse(); + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtilsClassLevelHibernateAndEclipseLinkHidingTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtilsClassLevelHibernateAndEclipseLinkHidingTests.java new file mode 100644 index 0000000000..25a238d2f9 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtilsClassLevelHibernateAndEclipseLinkHidingTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.provider; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * Test cases to verify {@link PersistenceProviderUtils} annotations work at the class level. + * + * @author Greg Turnquist + */ +@HidePersistenceProviders({ PersistenceProvider.HIBERNATE, PersistenceProvider.ECLIPSELINK }) +class PersistenceProviderUtilsClassLevelHibernateAndEclipseLinkHidingTests { + + @Test + void hidingHibernateAtTheClassLevelShouldWork() { + + assertThat(PersistenceProvider.HIBERNATE.isPresent()).isFalse(); + assertThat(PersistenceProvider.ECLIPSELINK.isPresent()).isFalse(); + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtilsClassLevelHibernateHidingTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtilsClassLevelHibernateHidingTests.java new file mode 100644 index 0000000000..466b042ed7 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtilsClassLevelHibernateHidingTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.provider; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * Test cases to verify {@link PersistenceProviderUtils} annotations work at the class level. + * + * @author Greg Turnquist + */ +@HidePersistenceProviders(PersistenceProvider.HIBERNATE) +class PersistenceProviderUtilsClassLevelHibernateHidingTests { + + @Test + void hidingHibernateAtTheClassLevelShouldWork() { + + assertThat(PersistenceProvider.HIBERNATE.isPresent()).isFalse(); + assertThat(PersistenceProvider.ECLIPSELINK.isPresent()).isTrue(); + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtilsTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtilsTests.java new file mode 100644 index 0000000000..1b4d618fad --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/provider/PersistenceProviderUtilsTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.provider; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.jpa.provider.PersistenceProviderUtils.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +/** + * Test cases to verify {@link PersistenceProviderUtils}. + * + * @author Greg Turnquist + */ +class PersistenceProviderUtilsTests { + + @Test + void hideHibernateWorks() { + + assertThat(PersistenceProvider.HIBERNATE.isPresent()).isTrue(); + + doWithHibernateHidden(() -> { + assertThat(PersistenceProvider.HIBERNATE.isPresent()).isFalse(); + }); + + assertThat(PersistenceProvider.HIBERNATE.isPresent()).isTrue(); + } + + @Test + void hideEclipseLinkWorks() { + + assertThat(PersistenceProvider.ECLIPSELINK.isPresent()).isTrue(); + + doWithEclipseLinkHidden(() -> { + assertThat(PersistenceProvider.ECLIPSELINK.isPresent()).isFalse(); + }); + + assertThat(PersistenceProvider.ECLIPSELINK.isPresent()).isTrue(); + } + + @Test + void hideHibernateAndEclipseLinkWorks() { + + assertThat(PersistenceProvider.HIBERNATE.isPresent()).isTrue(); + assertThat(PersistenceProvider.ECLIPSELINK.isPresent()).isTrue(); + + doWithPersistenceProvidersHidden(List.of(PersistenceProvider.HIBERNATE, PersistenceProvider.ECLIPSELINK), () -> { + + assertThat(PersistenceProvider.HIBERNATE.isPresent()).isFalse(); + assertThat(PersistenceProvider.ECLIPSELINK.isPresent()).isFalse(); + }); + + assertThat(PersistenceProvider.HIBERNATE.isPresent()).isTrue(); + assertThat(PersistenceProvider.ECLIPSELINK.isPresent()).isTrue(); + } + + @Test + @HidePersistenceProviders(PersistenceProvider.HIBERNATE) + void hideHibernateGenericallyShouldWork() { + + assertThat(PersistenceProvider.HIBERNATE.isPresent()).isFalse(); + assertThat(PersistenceProvider.ECLIPSELINK.isPresent()).isTrue(); + } + + @Test + @HidePersistenceProviders(PersistenceProvider.ECLIPSELINK) + void hideEclipseLinkGenericallyShouldWork() { + + assertThat(PersistenceProvider.HIBERNATE.isPresent()).isTrue(); + assertThat(PersistenceProvider.ECLIPSELINK.isPresent()).isFalse(); + } + + @Test + @HidePersistenceProviders({ PersistenceProvider.HIBERNATE, PersistenceProvider.ECLIPSELINK }) + void hideHibernateAndEclipseLinkGenericallyShouldWork() { + + assertThat(PersistenceProvider.HIBERNATE.isPresent()).isFalse(); + assertThat(PersistenceProvider.ECLIPSELINK.isPresent()).isFalse(); + } + + @Test + @HideHibernate + void hideHibernateViaHibernateSpecificAnnotationWorks() { + + assertThat(PersistenceProvider.HIBERNATE.isPresent()).isFalse(); + assertThat(PersistenceProvider.ECLIPSELINK.isPresent()).isTrue(); + } + + @Test + @HideEclipseLink + void hideEclipseLinkViaEclipseLinkSpecificAnnotationWorks() { + + assertThat(PersistenceProvider.HIBERNATE.isPresent()).isTrue(); + assertThat(PersistenceProvider.ECLIPSELINK.isPresent()).isFalse(); + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JpaSortUnsafeEqlTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JpaSortUnsafeEqlTests.java new file mode 100644 index 0000000000..973ac7ec3f --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JpaSortUnsafeEqlTests.java @@ -0,0 +1,145 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.jpa.domain.sample.UserSpecifications.*; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.JpaSort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.domain.sample.Role; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.provider.HideHibernate; +import org.springframework.data.jpa.repository.sample.SampleEvaluationContextExtension; +import org.springframework.data.jpa.repository.sample.UserRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +/** + * Verify that {@link JpaSort#unsafe(String...)} works properly with EclipseLink. + * + * @author Greg Turnquist + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration({ "classpath:application-context.xml", "classpath:eclipselink.xml" }) +@Transactional +@HideHibernate +class JpaSortUnsafeEqlTests { + + @PersistenceContext EntityManager em; + + // CUT + @Autowired UserRepository repository; + + // Test fixture + private User firstUser; + private User secondUser; + private User thirdUser; + private User fourthUser; + private Integer id; + private Role adminRole; + + @BeforeEach + void setUp() throws Exception { + + firstUser = new User("Oliver", "Gierke", "gierke@synyx.de"); + firstUser.setAge(28); + secondUser = new User("Joachim", "Arrasz", "arrasz@synyx.de"); + secondUser.setAge(35); + Thread.sleep(10); + thirdUser = new User("Dave", "Matthews", "no@email.com"); + thirdUser.setAge(43); + fourthUser = new User("kevin", "raymond", "no@gmail.com"); + fourthUser.setAge(31); + adminRole = new Role("admin"); + + SampleEvaluationContextExtension.SampleSecurityContextHolder.clear(); + } + + void flushTestUsers() { + + em.persist(adminRole); + + firstUser = repository.save(firstUser); + secondUser = repository.save(secondUser); + thirdUser = repository.save(thirdUser); + fourthUser = repository.save(fourthUser); + + repository.flush(); + + id = firstUser.getId(); + + assertThat(id).isNotNull(); + assertThat(secondUser.getId()).isNotNull(); + assertThat(thirdUser.getId()).isNotNull(); + assertThat(fourthUser.getId()).isNotNull(); + + assertThat(repository.existsById(id)).isTrue(); + assertThat(repository.existsById(secondUser.getId())).isTrue(); + assertThat(repository.existsById(thirdUser.getId())).isTrue(); + assertThat(repository.existsById(fourthUser.getId())).isTrue(); + } + + @Test // GH-3172 + void unsafeFindAllWithPageRequest() { + + flushTestUsers(); + + firstUser.setManager(firstUser); + secondUser.setManager(firstUser); + thirdUser.setManager(secondUser); + fourthUser.setManager(secondUser); + + repository.saveAllAndFlush(List.of(firstUser, secondUser, thirdUser, fourthUser)); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("firstname")))) + .containsExactly(thirdUser, secondUser, firstUser, fourthUser); + + // Path-based expression + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("manager.firstname")))) + .containsExactly(thirdUser, fourthUser, firstUser, secondUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("manager.manager.firstname")))) + .containsExactly(firstUser, secondUser, thirdUser, fourthUser); + + // Compound JpaOrder.unsafe operation + assertThat(repository.findAll(PageRequest.of(0, 4, JpaSort.unsafe("lastname") // + .and(JpaSort.unsafe("firstname").descending())))).containsExactly(secondUser, firstUser, thirdUser, fourthUser); + + // + // Also works with custom Specification + // + + Specification spec = userHasFirstname("Oliver").or(userHasLastname("Matthews")); + + assertThat(repository.findAll(spec, // + PageRequest.of(0, 4, JpaSort.unsafe("firstname")))).containsExactly(thirdUser, firstUser); + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JpaSortUnsafeHqlTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JpaSortUnsafeHqlTests.java new file mode 100644 index 0000000000..00800b8afa --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JpaSortUnsafeHqlTests.java @@ -0,0 +1,381 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.jpa.domain.sample.UserSpecifications.*; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.JpaSort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.domain.sample.Role; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.repository.sample.SampleEvaluationContextExtension.SampleSecurityContextHolder; +import org.springframework.data.jpa.repository.sample.UserRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +/** + * Verify that {@link JpaSort#unsafe(String...)} works properly with Hibernate. + * + * @author Greg Turnquist + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration("classpath:application-context.xml") +@Transactional +class JpaSortUnsafeHqlTests { + + @PersistenceContext EntityManager em; + + // CUT + @Autowired UserRepository repository; + + // Test fixture + private User firstUser; + private User secondUser; + private User thirdUser; + private User fourthUser; + private Integer id; + private Role adminRole; + + @BeforeEach + void setUp() throws Exception { + + firstUser = new User("Oliver", "Gierke", "gierke@synyx.de"); + firstUser.setAge(28); + secondUser = new User("Joachim", "Arrasz", "arrasz@synyx.de"); + secondUser.setAge(35); + Thread.sleep(10); + thirdUser = new User("Dave", "Matthews", "no@email.com"); + thirdUser.setAge(43); + fourthUser = new User("kevin", "raymond", "no@gmail.com"); + fourthUser.setAge(31); + adminRole = new Role("admin"); + + SampleSecurityContextHolder.clear(); + } + + void flushTestUsers() { + + em.persist(adminRole); + + firstUser = repository.save(firstUser); + secondUser = repository.save(secondUser); + thirdUser = repository.save(thirdUser); + fourthUser = repository.save(fourthUser); + + repository.flush(); + + id = firstUser.getId(); + + assertThat(id).isNotNull(); + assertThat(secondUser.getId()).isNotNull(); + assertThat(thirdUser.getId()).isNotNull(); + assertThat(fourthUser.getId()).isNotNull(); + + assertThat(repository.existsById(id)).isTrue(); + assertThat(repository.existsById(secondUser.getId())).isTrue(); + assertThat(repository.existsById(thirdUser.getId())).isTrue(); + assertThat(repository.existsById(fourthUser.getId())).isTrue(); + } + + @Test // GH-3172 + void unsafeFindAllWithPageRequest() { + + flushTestUsers(); + + firstUser.setManager(firstUser); + secondUser.setManager(firstUser); + thirdUser.setManager(secondUser); + fourthUser.setManager(secondUser); + + repository.saveAllAndFlush(List.of(firstUser, secondUser, thirdUser, fourthUser)); + + // Generic function calls with one or more arguments + assertThat(repository.findAll(PageRequest.of(0, 4, JpaSort.unsafe("LENGTH(firstname)")))).containsExactly(thirdUser, + fourthUser, firstUser, secondUser); + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("char_length(firstname)")))) + .containsExactly(thirdUser, fourthUser, firstUser, secondUser); + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("substring(emailAddress, 0, 3)")))) + .containsExactly(secondUser, firstUser, thirdUser, fourthUser); + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("repeat('a', 5)")))) + .containsExactly(firstUser, secondUser, thirdUser, fourthUser); + + // Trim function call + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("trim(leading '.' from lastname)")))) + .containsExactly(secondUser, firstUser, thirdUser, fourthUser); + + // Grouped expression + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("(firstname)")))) + .containsExactly(thirdUser, secondUser, firstUser, fourthUser); + + // Tuple argument + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("(firstname, lastname)"))); + }); + + // Subquery + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("(select e from Employee e)"))); + + }); + + // Literal expressions + assertThatExceptionOfType(InvalidDataAccessResourceUsageException.class).isThrownBy(() -> { + repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("'a'"))); + }); + + assertThatExceptionOfType(InvalidDataAccessResourceUsageException.class).isThrownBy(() -> { + repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("'abc'"))); + }); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("5")))).containsExactly(firstUser, secondUser, thirdUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("length('a')")))) + .containsExactly(firstUser, secondUser, thirdUser, fourthUser); + + // Parameters + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + repository.findAll(PageRequest.of(0, 4, JpaSort.unsafe(":name"))); + }); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + repository.findAll(PageRequest.of(0, 4, JpaSort.unsafe("?1"))); + }); + + // Arithmetic calls + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("LENGTH(firstname) + 5")))) + .containsExactly(thirdUser, fourthUser, firstUser, secondUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("LENGTH(firstname) - 1")))) + .containsExactly(thirdUser, fourthUser, firstUser, secondUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("LENGTH(firstname) * 5.0")))) + .containsExactly(thirdUser, fourthUser, firstUser, secondUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("LENGTH(firstname) / 5.0")))) + .containsExactly(thirdUser, firstUser, secondUser, fourthUser); + + // Concat operation + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("firstname || lastname")))) + .containsExactly(thirdUser, secondUser, firstUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("upper(firstname) || upper(lastname)")))) + .containsExactly(thirdUser, secondUser, fourthUser, firstUser); + + // Path-based expression + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("manager.firstname")))) + .containsExactly(thirdUser, fourthUser, firstUser, secondUser); + + // Compound JpaOrder.unsafe operation + assertThat(repository.findAll(PageRequest.of(0, 4, JpaSort.unsafe("LENGTH(lastname)") // + .and(JpaSort.unsafe("LENGTH(firstname)").descending())))) + .containsExactly(secondUser, firstUser, fourthUser, thirdUser); + + // Case-based expressions + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case firstname when 'Oliver' then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case firstname when 'Oliver' then 'z' else firstname end")))) + .containsExactly(thirdUser, secondUser, fourthUser, firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case firstname when 'Oliver' then 'A' when 'Joachim' then 'z' else firstname end")))) + .containsExactly(firstUser, thirdUser, fourthUser, secondUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case firstname when 'Oliver' then 'z' when 'Joachim' then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe( + "case when firstname = 'Oliver' then 'z' when firstname = 'Joachim' then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age < 31 then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age <= 31 then 'A' else firstname end")))) + .containsExactly(firstUser, fourthUser, thirdUser, secondUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age > 42 then 'A' else firstname end")))) + .containsExactly(thirdUser, secondUser, firstUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age >= 43 then 'A' else firstname end")))) + .containsExactly(thirdUser, secondUser, firstUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age <> 28 then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age != 28 then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age ^= 28 then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + // Hibernate doesn't support using function calls inside case predicates. + assertThatExceptionOfType(InvalidDataAccessResourceUsageException.class).isThrownBy(() -> { + repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when LENGTH(firstname) = 6 then 'A' else firstname end"))); + }); + + // Case when IS NOT? NULL expressions + firstUser.setManager(null); + secondUser.setManager(firstUser); + thirdUser.setManager(firstUser); + fourthUser.setManager(firstUser); + + repository.saveAllAndFlush(List.of(firstUser, secondUser, thirdUser, fourthUser)); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when manager is null then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when manager is not null then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + // Case IS NOT? DISTINCT FROM expression + firstUser.setLastname(firstUser.getFirstname()); + repository.saveAndFlush(firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case when firstname is distinct from lastname then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case when firstname is not distinct from lastname then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + // Case NOT? IN + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when firstname in ('Oliver', 'Dave') then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case when firstname not in ('Oliver', 'Dave') then 'A' else firstname end")))) + .containsExactly(secondUser, fourthUser, thirdUser, firstUser); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case when firstname in ELEMENTS (manager.firstname) then 'A' else firstname end"))); + }); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case when firstname in (select u.firstname from User u) then 'A' else firstname end"))); + }); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when firstname in :names then 'A' else firstname end"))); + }); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age between 25 and 30 then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age not between 25 and 30 then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when firstname like 'O%' then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when firstname not like 'O%' then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when firstname ilike 'O%' then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when firstname not ilike 'O%' then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when firstname like 'O%' escape '^' then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case when firstname not like 'O%' escape '^' then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when firstname ilike 'O%' escape '^' then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case when firstname not ilike 'O%' escape '^' then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + // + // Also works with custom Specification + // + + Specification spec = userHasFirstname("Oliver").or(userHasLastname("Matthews")); + + assertThat(repository.findAll(spec, // + PageRequest.of(0, 4, JpaSort.unsafe("LENGTH(firstname)")))).containsExactly(thirdUser, firstUser); + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JpaSortUnsafeJpqlOnEclipseLinkTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JpaSortUnsafeJpqlOnEclipseLinkTests.java new file mode 100644 index 0000000000..4062c4222f --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JpaSortUnsafeJpqlOnEclipseLinkTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.jpa.domain.sample.UserSpecifications.*; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.JpaSort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.domain.sample.Role; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.provider.HidePersistenceProviders; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.sample.SampleEvaluationContextExtension; +import org.springframework.data.jpa.repository.sample.UserRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +/** + * Verify that {@link JpaSort#unsafe(String...)} works properly with JPQL on EclipseLink. + * + * @author Greg Turnquist + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration({ "classpath:application-context.xml", "classpath:eclipselink.xml" }) +@Transactional +@HidePersistenceProviders({ PersistenceProvider.HIBERNATE, PersistenceProvider.ECLIPSELINK }) +class JpaSortUnsafeJpqlOnEclipseLinkTests { + + @PersistenceContext EntityManager em; + + // CUT + @Autowired UserRepository repository; + + // Test fixture + private User firstUser; + private User secondUser; + private User thirdUser; + private User fourthUser; + private Integer id; + private Role adminRole; + + @BeforeEach + void setUp() throws Exception { + + firstUser = new User("Oliver", "Gierke", "gierke@synyx.de"); + firstUser.setAge(28); + secondUser = new User("Joachim", "Arrasz", "arrasz@synyx.de"); + secondUser.setAge(35); + Thread.sleep(10); + thirdUser = new User("Dave", "Matthews", "no@email.com"); + thirdUser.setAge(43); + fourthUser = new User("kevin", "raymond", "no@gmail.com"); + fourthUser.setAge(31); + adminRole = new Role("admin"); + + SampleEvaluationContextExtension.SampleSecurityContextHolder.clear(); + } + + void flushTestUsers() { + + em.persist(adminRole); + + firstUser = repository.save(firstUser); + secondUser = repository.save(secondUser); + thirdUser = repository.save(thirdUser); + fourthUser = repository.save(fourthUser); + + repository.flush(); + + id = firstUser.getId(); + + assertThat(id).isNotNull(); + assertThat(secondUser.getId()).isNotNull(); + assertThat(thirdUser.getId()).isNotNull(); + assertThat(fourthUser.getId()).isNotNull(); + + assertThat(repository.existsById(id)).isTrue(); + assertThat(repository.existsById(secondUser.getId())).isTrue(); + assertThat(repository.existsById(thirdUser.getId())).isTrue(); + assertThat(repository.existsById(fourthUser.getId())).isTrue(); + } + + @Test // GH-3172 + void unsafeFindAllWithPageRequest() { + + flushTestUsers(); + + firstUser.setManager(firstUser); + secondUser.setManager(firstUser); + thirdUser.setManager(secondUser); + fourthUser.setManager(secondUser); + + repository.saveAllAndFlush(List.of(firstUser, secondUser, thirdUser, fourthUser)); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("firstname")))) + .containsExactly(thirdUser, secondUser, firstUser, fourthUser); + + // Path-based expression + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("manager.firstname")))) + .containsExactly(thirdUser, fourthUser, firstUser, secondUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("manager.manager.firstname")))) + .containsExactly(firstUser, secondUser, thirdUser, fourthUser); + + // Compound JpaOrder.unsafe operation + assertThat(repository.findAll(PageRequest.of(0, 4, JpaSort.unsafe("lastname") // + .and(JpaSort.unsafe("firstname").descending())))).containsExactly(secondUser, firstUser, thirdUser, fourthUser); + + // + // Also works with custom Specification + // + + Specification spec = userHasFirstname("Oliver").or(userHasLastname("Matthews")); + + assertThat(repository.findAll(spec, // + PageRequest.of(0, 4, JpaSort.unsafe("firstname")))).containsExactly(thirdUser, firstUser); + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JpaSortUnsafeJpqlOnHibernateTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JpaSortUnsafeJpqlOnHibernateTests.java new file mode 100644 index 0000000000..0445b7783f --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/JpaSortUnsafeJpqlOnHibernateTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.jpa.domain.sample.UserSpecifications.*; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.JpaSort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.domain.sample.Role; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.provider.HidePersistenceProviders; +import org.springframework.data.jpa.provider.PersistenceProvider; +import org.springframework.data.jpa.repository.sample.SampleEvaluationContextExtension; +import org.springframework.data.jpa.repository.sample.UserRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +/** + * Verify that {@link JpaSort#unsafe(String...)} works properly with JPQL on Hibernate. + * + * @author Greg Turnquist + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration("classpath:application-context.xml") +@Transactional +@HidePersistenceProviders({ PersistenceProvider.HIBERNATE, PersistenceProvider.ECLIPSELINK }) +class JpaSortUnsafeJpqlOnHibernateTests { + + @PersistenceContext EntityManager em; + + // CUT + @Autowired UserRepository repository; + + // Test fixture + private User firstUser; + private User secondUser; + private User thirdUser; + private User fourthUser; + private Integer id; + private Role adminRole; + + @BeforeEach + void setUp() throws Exception { + + firstUser = new User("Oliver", "Gierke", "gierke@synyx.de"); + firstUser.setAge(28); + secondUser = new User("Joachim", "Arrasz", "arrasz@synyx.de"); + secondUser.setAge(35); + Thread.sleep(10); + thirdUser = new User("Dave", "Matthews", "no@email.com"); + thirdUser.setAge(43); + fourthUser = new User("kevin", "raymond", "no@gmail.com"); + fourthUser.setAge(31); + adminRole = new Role("admin"); + + SampleEvaluationContextExtension.SampleSecurityContextHolder.clear(); + } + + void flushTestUsers() { + + em.persist(adminRole); + + firstUser = repository.save(firstUser); + secondUser = repository.save(secondUser); + thirdUser = repository.save(thirdUser); + fourthUser = repository.save(fourthUser); + + repository.flush(); + + id = firstUser.getId(); + + assertThat(id).isNotNull(); + assertThat(secondUser.getId()).isNotNull(); + assertThat(thirdUser.getId()).isNotNull(); + assertThat(fourthUser.getId()).isNotNull(); + + assertThat(repository.existsById(id)).isTrue(); + assertThat(repository.existsById(secondUser.getId())).isTrue(); + assertThat(repository.existsById(thirdUser.getId())).isTrue(); + assertThat(repository.existsById(fourthUser.getId())).isTrue(); + } + + @Test // GH-3172 + void unsafeFindAllWithPageRequest() { + + flushTestUsers(); + + firstUser.setManager(firstUser); + secondUser.setManager(firstUser); + thirdUser.setManager(secondUser); + fourthUser.setManager(secondUser); + + repository.saveAllAndFlush(List.of(firstUser, secondUser, thirdUser, fourthUser)); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("firstname")))) + .containsExactly(thirdUser, secondUser, firstUser, fourthUser); + + // Path-based expression + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("manager.firstname")))) + .containsExactly(thirdUser, fourthUser, firstUser, secondUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("manager.manager.firstname")))) + .containsExactly(firstUser, secondUser, thirdUser, fourthUser); + + // Compound JpaOrder.unsafe operation + assertThat(repository.findAll(PageRequest.of(0, 4, JpaSort.unsafe("lastname") // + .and(JpaSort.unsafe("firstname").descending())))).containsExactly(secondUser, firstUser, thirdUser, fourthUser); + + // + // Also works with custom Specification + // + + Specification spec = userHasFirstname("Oliver").or(userHasLastname("Matthews")); + + assertThat(repository.findAll(spec, // + PageRequest.of(0, 4, JpaSort.unsafe("firstname")))).containsExactly(thirdUser, firstUser); + } +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index 9264185849..054323056a 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -54,9 +54,11 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.data.domain.*; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; +import org.springframework.data.jpa.domain.JpaSort; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.sample.Address; import org.springframework.data.jpa.domain.sample.QUser; @@ -3344,4 +3346,280 @@ private Page executeSpecWithSort(Sort sort) { private interface UserProjectionInterfaceBased { String getFirstname(); } + + @Test // GH-3172 + void unsafeFindAllWithPageRequest() { + + flushTestUsers(); + + firstUser.setManager(firstUser); + secondUser.setManager(firstUser); + thirdUser.setManager(secondUser); + fourthUser.setManager(secondUser); + + repository.saveAllAndFlush(List.of(firstUser, secondUser, thirdUser, fourthUser)); + + // Generic function calls with one or more arguments + assertThat(repository.findAll(PageRequest.of(0, 4, JpaSort.unsafe("LENGTH(firstname)")))).containsExactly(thirdUser, + fourthUser, firstUser, secondUser); + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("char_length(firstname)")))) + .containsExactly(thirdUser, fourthUser, firstUser, secondUser); + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("substring(emailAddress, 0, 3)")))) + .containsExactly(secondUser, firstUser, thirdUser, fourthUser); + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("repeat('a', 5)")))) + .containsExactly(firstUser, secondUser, thirdUser, fourthUser); + + // Trim function call + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("trim(leading '.' from lastname)")))) + .containsExactly(secondUser, firstUser, thirdUser, fourthUser); + + // Grouped expression + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("(firstname)")))) + .containsExactly(thirdUser, secondUser, firstUser, fourthUser); + + // Tuple argument + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("(firstname, lastname)"))); + }); + + // Subquery + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("(select e from Employee e)"))); + + }); + + // Literal expressions + assertThatExceptionOfType(InvalidDataAccessResourceUsageException.class).isThrownBy(() -> { + repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("'a'"))); + }); + + assertThatExceptionOfType(InvalidDataAccessResourceUsageException.class).isThrownBy(() -> { + repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("'abc'"))); + }); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("5")))).containsExactly(firstUser, secondUser, thirdUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("length('a')")))) + .containsExactly(firstUser, secondUser, thirdUser, fourthUser); + + // Parameters + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + repository.findAll(PageRequest.of(0, 4, JpaSort.unsafe(":name"))); + }); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + repository.findAll(PageRequest.of(0, 4, JpaSort.unsafe("?1"))); + }); + + // Arithmetic calls + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("LENGTH(firstname) + 5")))) + .containsExactly(thirdUser, fourthUser, firstUser, secondUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("LENGTH(firstname) - 1")))) + .containsExactly(thirdUser, fourthUser, firstUser, secondUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("LENGTH(firstname) * 5.0")))) + .containsExactly(thirdUser, fourthUser, firstUser, secondUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("LENGTH(firstname) / 5.0")))) + .containsExactly(thirdUser, firstUser, secondUser, fourthUser); + + // Concat operation + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("firstname || lastname")))) + .containsExactly(thirdUser, secondUser, firstUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("upper(firstname) || upper(lastname)")))) + .containsExactly(thirdUser, secondUser, fourthUser, firstUser); + + // Path-based expression + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("manager.firstname")))) + .containsExactly(thirdUser, fourthUser, firstUser, secondUser); + + // Compound JpaOrder.unsafe operation + assertThat(repository.findAll(PageRequest.of(0, 4, JpaSort.unsafe("LENGTH(lastname)") // + .and(JpaSort.unsafe("LENGTH(firstname)").descending())))) + .containsExactly(secondUser, firstUser, fourthUser, thirdUser); + + // Case-based expressions + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case firstname when 'Oliver' then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case firstname when 'Oliver' then 'z' else firstname end")))) + .containsExactly(thirdUser, secondUser, fourthUser, firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case firstname when 'Oliver' then 'A' when 'Joachim' then 'z' else firstname end")))) + .containsExactly(firstUser, thirdUser, fourthUser, secondUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case firstname when 'Oliver' then 'z' when 'Joachim' then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe( + "case when firstname = 'Oliver' then 'z' when firstname = 'Joachim' then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age < 31 then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age <= 31 then 'A' else firstname end")))) + .containsExactly(firstUser, fourthUser, thirdUser, secondUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age > 42 then 'A' else firstname end")))) + .containsExactly(thirdUser, secondUser, firstUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age >= 43 then 'A' else firstname end")))) + .containsExactly(thirdUser, secondUser, firstUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age <> 28 then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age != 28 then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age ^= 28 then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + // Hibernate doesn't support using function calls inside case predicates. + assertThatExceptionOfType(InvalidDataAccessResourceUsageException.class).isThrownBy(() -> { + repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when LENGTH(firstname) = 6 then 'A' else firstname end"))); + }); + + // Case when IS NOT? NULL expressions + firstUser.setManager(null); + secondUser.setManager(firstUser); + thirdUser.setManager(firstUser); + fourthUser.setManager(firstUser); + + repository.saveAllAndFlush(List.of(firstUser, secondUser, thirdUser, fourthUser)); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when manager is null then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when manager is not null then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + // Case IS NOT? DISTINCT FROM expression + firstUser.setLastname(firstUser.getFirstname()); + repository.saveAndFlush(firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case when firstname is distinct from lastname then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case when firstname is not distinct from lastname then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + // Case NOT? IN + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when firstname in ('Oliver', 'Dave') then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case when firstname not in ('Oliver', 'Dave') then 'A' else firstname end")))) + .containsExactly(secondUser, fourthUser, thirdUser, firstUser); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case when firstname in ELEMENTS (manager.firstname) then 'A' else firstname end"))); + }); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case when firstname in (select u.firstname from User u) then 'A' else firstname end"))); + }); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when firstname in :names then 'A' else firstname end"))); + }); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age between 25 and 30 then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when age not between 25 and 30 then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when firstname like 'O%' then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when firstname not like 'O%' then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when firstname ilike 'O%' then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when firstname not ilike 'O%' then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when firstname like 'O%' escape '^' then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case when firstname not like 'O%' escape '^' then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, JpaSort.unsafe("case when firstname ilike 'O%' escape '^' then 'A' else firstname end")))) + .containsExactly(firstUser, thirdUser, secondUser, fourthUser); + + assertThat(repository.findAll( // + PageRequest.of(0, 4, + JpaSort.unsafe("case when firstname not ilike 'O%' escape '^' then 'A' else firstname end")))) + .containsExactly(secondUser, thirdUser, fourthUser, firstUser); + + // + // Also works with custom Specification + // + + Specification spec = userHasFirstname("Oliver").or(userHasLastname("Matthews")); + + assertThat(repository.findAll(spec, // + PageRequest.of(0, 4, JpaSort.unsafe("LENGTH(firstname)")))).containsExactly(thirdUser, firstUser); + } } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java index ef90549fd4..047a875775 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerFactoryUnitTests.java @@ -18,6 +18,9 @@ import static org.assertj.core.api.Assertions.*; import org.junit.jupiter.api.Test; +import org.springframework.data.jpa.provider.HideHibernate; +import org.springframework.data.jpa.provider.HidePersistenceProviders; +import org.springframework.data.jpa.provider.PersistenceProvider; /** * Unit tests for {@link QueryEnhancerFactory}. @@ -28,18 +31,35 @@ class QueryEnhancerFactoryUnitTests { @Test - void createsParsingImplementationForNonNativeQuery() { + void nonNativeQueryPicksHqlParserWhenHibernateOnClasspath() { - StringQuery query = new StringQuery("select new com.example.User(u.firstname) from User u", false); + QueryEnhancer queryEnhancer = QueryEnhancerFactory + .forQuery(new StringQuery("select new com.example.User(u.firstname) from User u", false)); - QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(query); + assertThat(queryEnhancer).isInstanceOf(JpaQueryEnhancer.class); + assertThat(((JpaQueryEnhancer) queryEnhancer).getQueryParsingStrategy()).isInstanceOf(HqlQueryParser.class); + } - assertThat(queryEnhancer) // - .isInstanceOf(JpaQueryEnhancer.class); + @Test // GH-3172 + @HideHibernate + void nonNativeQueryPicksEqlParserWhenEclipseLinkOnClasspath() { + + QueryEnhancer queryEnhancer = QueryEnhancerFactory + .forQuery(new StringQuery("select new com.example.User(u.firstname) from User u", false)); + + assertThat(queryEnhancer).isInstanceOf(JpaQueryEnhancer.class); + assertThat(((JpaQueryEnhancer) queryEnhancer).getQueryParsingStrategy()).isInstanceOf(EqlQueryParser.class); + } + + @Test // GH-3172 + @HidePersistenceProviders({ PersistenceProvider.HIBERNATE, PersistenceProvider.ECLIPSELINK }) + void nonNativeQueryPicksJpqlParserWhenHibernateAndEclipseLinkAreNotOnClasspath() { - JpaQueryEnhancer queryParsingEnhancer = (JpaQueryEnhancer) queryEnhancer; + QueryEnhancer queryEnhancer = QueryEnhancerFactory + .forQuery(new StringQuery("select new com.example.User(u.firstname) from User u", false)); - assertThat(queryParsingEnhancer.getQueryParsingStrategy()).isInstanceOf(HqlQueryParser.class); + assertThat(queryEnhancer).isInstanceOf(JpaQueryEnhancer.class); + assertThat(((JpaQueryEnhancer) queryEnhancer).getQueryParsingStrategy()).isInstanceOf(JpqlQueryParser.class); } @Test