From 8dbbe553e1e685face87795286abde07181644bb Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 5 Jul 2017 14:28:26 +0200 Subject: [PATCH] DATAJPA-1140 - Allow mixing of SpEL expression and normal query parameters. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructured the complete parameter binding. There are now two principle ways to perform the binding: - if no query is known to the binder the method parameters get iterated and assigned to query parameters. This is implemented by the bind method of the ParameterBinder. - if a query is known all the bindings, i.e the query parameters are iterated and matching values are searched. This is implemented in the QueryAwareParameterBinder. For finding a suitable value there exists various QueryParameterSetterStrategies. The code calling the various Query.setParameter(…) overloads is extracted and gathered in QueryParameterSetter. Original pull request: #206. --- .../query/AbstractStringBasedJpaQuery.java | 5 +- .../query/CriteriaQueryParameterBinder.java | 16 +- .../jpa/repository/query/ParameterBinder.java | 96 +++------- .../query/QueryAwareParameterBinder.java | 86 +++++++++ .../query/QueryParameterSetter.java | 157 ++++++++++++++++ .../query/QueryParameterSetterStrategy.java | 154 ++++++++++++++++ ...lExpressionStringQueryParameterBinder.java | 171 ------------------ .../jpa/repository/query/StringQuery.java | 9 +- .../query/StringQueryParameterBinder.java | 97 ---------- .../query/ParameterBinderUnitTests.java | 17 +- 10 files changed, 449 insertions(+), 359 deletions(-) create mode 100644 src/main/java/org/springframework/data/jpa/repository/query/QueryAwareParameterBinder.java create mode 100644 src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java create mode 100644 src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterStrategy.java delete mode 100644 src/main/java/org/springframework/data/jpa/repository/query/SpelExpressionStringQueryParameterBinder.java delete mode 100644 src/main/java/org/springframework/data/jpa/repository/query/StringQueryParameterBinder.java diff --git a/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java index 7a0392edb..fcf757f29 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2016 the original author or authors. + * Copyright 2008-2017 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. @@ -32,6 +32,7 @@ import org.springframework.util.Assert; * * @author Oliver Gierke * @author Thomas Darimont + * @author Jens Schauder */ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { @@ -87,7 +88,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { */ @Override protected ParameterBinder createBinder(Object[] values) { - return new SpelExpressionStringQueryParameterBinder(getQueryMethod().getParameters(), values, query, + return new QueryAwareParameterBinder(getQueryMethod().getParameters(), values, query, evaluationContextProvider, parser); } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/CriteriaQueryParameterBinder.java b/src/main/java/org/springframework/data/jpa/repository/query/CriteriaQueryParameterBinder.java index 9b192e718..99de54c1a 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/CriteriaQueryParameterBinder.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/CriteriaQueryParameterBinder.java @@ -15,15 +15,13 @@ */ package org.springframework.data.jpa.repository.query; -import java.util.Date; import java.util.Iterator; -import javax.persistence.Parameter; -import javax.persistence.Query; import javax.persistence.criteria.CriteriaQuery; import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata; +import org.springframework.data.jpa.repository.query.QueryParameterSetter.ParameterExpressionQueryParameterSetter; import org.springframework.data.repository.query.Parameters; import org.springframework.util.Assert; @@ -33,6 +31,7 @@ import org.springframework.util.Assert; * @author Oliver Gierke * @author Thomas Darimont * @author Mark Paluch + * @author Jens Schauder */ class CriteriaQueryParameterBinder extends ParameterBinder { @@ -60,19 +59,18 @@ class CriteriaQueryParameterBinder extends ParameterBinder { */ @Override @SuppressWarnings("unchecked") - protected void bind(Query query, JpaParameter parameter, Object value, int position) { + protected QueryParameterSetter bind(JpaParameter parameter, Object value, Integer position) { ParameterMetadata metadata = (ParameterMetadata) expressions.next(); if (metadata.isIsNullParameter()) { - return; + return QueryParameterSetter.NOOP; } if (parameter.isTemporalParameter()) { - query.setParameter((Parameter) (Object) metadata.getExpression(), (Date) metadata.prepare(value), - parameter.getTemporalType()); - } else { - query.setParameter(metadata.getExpression(), metadata.prepare(value)); + return new ParameterExpressionQueryParameterSetter(metadata.getExpression(), parameter.getTemporalType(), metadata.prepare(value)); } + + return new ParameterExpressionQueryParameterSetter(metadata.getExpression(), null, metadata.prepare(value)); } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinder.java b/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinder.java index 0d28caef3..b1401cf7c 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinder.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/ParameterBinder.java @@ -15,8 +15,6 @@ */ package org.springframework.data.jpa.repository.query; -import java.util.Date; - import javax.persistence.Query; import org.springframework.data.domain.Pageable; @@ -35,18 +33,19 @@ import org.springframework.util.Assert; * @author Thomas Darimont * @author Mark Paluch * @author Christoph Strobl + * @author Jens Schauder */ public class ParameterBinder { + /** Meta information for the method parameters */ private final JpaParameters parameters; - private final ParameterAccessor accessor; - private final Object[] values; + protected final ParameterAccessor accessor; /** * Creates a new {@link ParameterBinder}. * * @param parameters must not be {@literal null}. - * @param values must not be {@literal null}. + * @param values values of the parameters passed to the method. Must not be {@literal null}. */ public ParameterBinder(JpaParameters parameters, Object[] values) { @@ -56,8 +55,7 @@ public class ParameterBinder { Assert.isTrue(parameters.getNumberOfParameters() == values.length, "Invalid number of parameters given!"); this.parameters = parameters; - this.values = values.clone(); - this.accessor = new ParametersParameterAccessor(parameters, this.values); + this.accessor = new ParametersParameterAccessor(parameters, values.clone()); } ParameterBinder(JpaParameters parameters) { @@ -66,20 +64,16 @@ public class ParameterBinder { /** * Returns the {@link Pageable} of the parameters, if available. Returns {@code null} otherwise. - * - * @return */ - public Pageable getPageable() { + Pageable getPageable() { return accessor.getPageable(); } /** * Returns the sort instance to be used for query creation. Will use a {@link Sort} parameter if available or the * {@link Sort} contained in a {@link Pageable} if available. Returns {@code null} if no {@link Sort} can be found. - * - * @return */ - public Sort getSort() { + Sort getSort() { return accessor.getSort(); } @@ -87,21 +81,21 @@ public class ParameterBinder { * Binds the parameters to the given {@link Query}. * * @param query must not be {@literal null}. - * @return */ - public T bind(T query) { + protected T bind(T query) { Assert.notNull(query, "Query must not be null!"); int bindableParameterIndex = 0; - int queryParameterPosition = 1; for (JpaParameter parameter : parameters) { - if (canBindParameter(parameter)) { + if (parameter.isBindable()) { Object value = accessor.getBindableValue(bindableParameterIndex); - bind(query, parameter, value, queryParameterPosition++); + + bind(parameter, value, bindableParameterIndex + 1).setParameter(query); + bindableParameterIndex++; } } @@ -110,62 +104,33 @@ public class ParameterBinder { } /** - * Returns {@literal true} if the given parameter can be bound. - * - * @param parameter - * @return + * Creates {@link QueryParameterSetter} for the given {@link JpaParameter}. This implementation uses the name or index + * of the passed in parameter. This method is intended to be overwritten by subclasses in order to implement + * alternative binding strategies. + * + * @param parameter Method parameter from which to create a {@link QueryParameterSetter} + * @param value The value of the method parameter. + * @param position the index of the query parameter. Note that there is no simple relationg between index of the + * method parameter and the position of the bind parameter due to unbindable parameters. + * @return guaranteed not to be {@literal null}. */ - protected boolean canBindParameter(JpaParameter parameter) { - return parameter.isBindable(); - } + protected QueryParameterSetter bind(JpaParameter parameter, Object value, Integer position) { - /** - * Perform the actual query parameter binding. - * - * @param query - * @param parameter - * @param value - * @param position - */ - protected void bind(Query query, JpaParameter parameter, Object value, int position) { - - if (parameter.isTemporalParameter()) { - if (hasNamedParameter(query) && parameter.isNamedParameter()) { - query.setParameter( - parameter.getName().orElseThrow(() -> new IllegalArgumentException("o_O paraneter needs to have a name!")), - (Date) value, parameter.getTemporalType()); - } else { - query.setParameter(position, (Date) value, parameter.getTemporalType()); - } - return; - } - - if (hasNamedParameter(query) && parameter.isNamedParameter()) { - query.setParameter( - parameter.getName().orElseThrow(() -> new IllegalArgumentException("o_O paraneter needs to have a name!")), - value); - } else { - query.setParameter(position, value); - } + return QueryParameterSetter.fromJpaParameter(parameter, position, value); } /** * Binds the parameters to the given query and applies special parameter types (e.g. pagination). * * @param query must not be {@literal null}. - * @return */ - public Query bindAndPrepare(Query query) { + Query bindAndPrepare(Query query) { Assert.notNull(query, "Query must not be null!"); return bindAndPrepare(query, parameters); } - boolean hasNamedParameter(Query query) { - return QueryUtils.hasNamedParameter(query); - } - private Query bindAndPrepare(Query query, Parameters parameters) { Query result = bind(query); @@ -179,17 +144,4 @@ public class ParameterBinder { return result; } - - /** - * Returns the parameters. - * - * @return - */ - JpaParameters getParameters() { - return parameters; - } - - protected Object[] getValues() { - return values; - } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/QueryAwareParameterBinder.java b/src/main/java/org/springframework/data/jpa/repository/query/QueryAwareParameterBinder.java new file mode 100644 index 000000000..cb5915a3d --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/QueryAwareParameterBinder.java @@ -0,0 +1,86 @@ +/* + * Copyright 2013-2017 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 + * + * http://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 java.util.ArrayList; +import java.util.List; + +import javax.persistence.Query; + +import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding; +import org.springframework.data.repository.query.EvaluationContextProvider; +import org.springframework.data.repository.query.Parameters; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; + +/** + * {@link ParameterBinder} has a {@link StringQuery} and therefor knows which query parameters need values. For binding + * values to a query it iterates the required bindings and tries to find a value for each binding. For finding those + * values it uses a list of strategies. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @author Christoph Strobl + * @author Jens Schauder + */ +class QueryAwareParameterBinder extends ParameterBinder { + + private final StringQuery query; + + private final List queryParameterSetterStrategies = new ArrayList<>(); + + /** + * Creates a new {@link QueryAwareParameterBinder} from the given {@link Parameters}, method arguments and + * {@link StringQuery}. + * + * @param parameters must not be {@literal null}. + * @param values must not be {@literal null}. + * @param query must not be {@literal null}. + */ + QueryAwareParameterBinder(JpaParameters parameters, Object[] values, StringQuery query, + EvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) { + + super(parameters, values); + + Assert.notNull(query, "StringQuery must not be null!"); + this.query = query; + + queryParameterSetterStrategies + .add(new QueryParameterSetterStrategy.ExpressionParameterStrategy(evaluationContextProvider, parser, parameters, values.clone())); + queryParameterSetterStrategies.add(new QueryParameterSetterStrategy.BasicParameterStrategy(parameters, accessor)); + } + + @Override + public T bind(T jpaQuery) { + + for (ParameterBinding binding : query.getParameterBindings()) { + createQueryParameterSetter(jpaQuery, binding).setParameter(jpaQuery); + } + + return jpaQuery; + } + + private QueryParameterSetter createQueryParameterSetter(T jpaQuery, ParameterBinding binding) { + + for (QueryParameterSetterStrategy strategy : queryParameterSetterStrategies) { + QueryParameterSetter candidate = strategy.create(jpaQuery, binding); + if (candidate != null) + return candidate; + } + + return QueryParameterSetter.NOOP; + } +} diff --git a/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java b/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java new file mode 100644 index 000000000..3165d5526 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetter.java @@ -0,0 +1,157 @@ +/* + * Copyright 2017 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 + * + * http://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 java.util.Date; + +import javax.persistence.Query; +import javax.persistence.TemporalType; +import javax.persistence.criteria.ParameterExpression; + +import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; + +/** + * The interface encapsulates the setting of query parameters which might use a significant number of variations of + * {@literal Query.setParameter}. + * + * @author Jens Schauder + * @since 2.0 + */ +interface QueryParameterSetter { + + void setParameter(Query query); + + /** Noop implementation */ + QueryParameterSetter NOOP = query -> {}; + + /** + * Creates a {@link QueryParameterSetter} from a {@link JpaParameter}. Handles named and indexed parameters and + * TemporalType annotations. + * + * @param parameter the method parameter to bind + * @param position position of the bind parameter, might be {@literal null} only if parameters are named. + * @param value the value passed to the method parameter. + */ + static QueryParameterSetter fromJpaParameter(JpaParameter parameter, Integer position, Object value) { + + TemporalType temporalType = parameter.isTemporalParameter() ? parameter.getTemporalType() : null; + String name = parameter.isNamedParameter() + ? parameter.getName().orElseThrow(() -> new IllegalArgumentException("o_O parameter needs to have a name!")) + : null; + return new NamedOrIndexedQueryParameterSetter(name, position, temporalType, value); + } + + /** + * Creates a {@link QueryParameterSetter} which ignores {@link IllegalArgumentException}s when calling + * {@literal setParameter}. Useful because certain JPA implementations do not correctly report the presence of + * parameters in a query. + * + * @param name the name of the parameter to bind. Might be {@literal null}. + * @param position position of the bind parameter, might be {@literal null} only if parameters are named. + * @param value the value passed to the method parameter. + */ + static QueryParameterSetter lenientQueryParameterSetter(String name, Integer position, Object value) { + + return (Query q) -> { + try { + if (name != null) { + q.setParameter(name, value); + } else { + q.setParameter(position, value); + } + } catch (IllegalArgumentException iae) { + + // Since Eclipse doesn't reliably report whether a query has parameters + // we simply try to set the parameters and ignore possible failures. + } + }; + } + + /** + * {@link QueryParameterSetter} for named or indexed parameters that might have a {@link TemporalType} specified. + */ + class NamedOrIndexedQueryParameterSetter implements QueryParameterSetter { + + private final boolean useParameterAccessByName; + private final String name; + private final Integer index; + private final TemporalType temporalType; + private final Object value; + + /** + * @param name of the parameter, if {@literal null} index based parameter access will be used. + * @param index index of the parameter. Only used when {@literal name) is {@literal null} + * @param temporalType can be {@literal null} + * @param value the value to be set, might be {@literal null} + */ + NamedOrIndexedQueryParameterSetter(String name, Integer index, TemporalType temporalType, Object value) { + + this.useParameterAccessByName = name != null; + this.name = name; + this.index = index; + this.temporalType = temporalType; + this.value = value; + } + + public void setParameter(Query query) { + + if (temporalType != null) { + + if (useParameterAccessByName && QueryUtils.hasNamedParameter(query)) { + query.setParameter(name, (Date) value, temporalType); + } else { + query.setParameter(index, (Date) value, temporalType); + } + } else { + + if (useParameterAccessByName && QueryUtils.hasNamedParameter(query)) { + query.setParameter(name, value); + } else { + query.setParameter(index, value); + } + } + } + } + + /** + * {@link QueryParameterSetter} identifying parameters by {@link ParameterExpression} + */ + class ParameterExpressionQueryParameterSetter implements QueryParameterSetter { + + private final ParameterExpression parameterExpression; + private final TemporalType temporalType; + private final Object value; + + ParameterExpressionQueryParameterSetter(ParameterExpression parameterExpression, TemporalType temporalType, + Object value) { + + this.temporalType = temporalType; + this.value = value; + this.parameterExpression = parameterExpression; + } + + @SuppressWarnings("unchecked") + public void setParameter(Query query) { + if (temporalType != null) { + + query.setParameter(parameterExpression, (Date) value, temporalType); + } else { + + query.setParameter(parameterExpression, value); + } + } + } +} diff --git a/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterStrategy.java b/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterStrategy.java new file mode 100644 index 000000000..0eabe34a9 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterStrategy.java @@ -0,0 +1,154 @@ +/* + * Copyright 2017 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 + * + * http://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 java.util.function.Predicate; + +import javax.persistence.Query; + +import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; +import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding; +import org.springframework.data.repository.query.EvaluationContextProvider; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.Parameters; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; + +/** + * Encapsulates different strategies for the creation of a {@link QueryParameterSetter} from a {@link Query} and a + * {@link ParameterBinding} + * + * @author Jens Schauder + * @since 2.0 + */ +interface QueryParameterSetterStrategy { + + QueryParameterSetter create(Query jpaQuery, ParameterBinding binding); + + /** + * Handles bindings that are SpEL expressions by evalutating the expression to obtain a value. + * + * @author Jens Schauder + */ + class ExpressionParameterStrategy implements QueryParameterSetterStrategy { + + private final EvaluationContextProvider evaluationContextProvider; + private final SpelExpressionParser parser; + private final Parameters parameters; + private final Object[] values; + + ExpressionParameterStrategy(EvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser, + Parameters parameters, Object[] values) { + + this.parameters = parameters; + this.values = values; + + Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!"); + Assert.notNull(parser, "SpelExpressionParser must not be null!"); + this.evaluationContextProvider = evaluationContextProvider; + this.parser = parser; + } + + @Override + public QueryParameterSetter create(Query jpaQuery, ParameterBinding binding) { + + if (binding.isExpression()) { + + Expression expr = parseExpressionString(binding.getExpression()); + + Object value = evaluateExpression(expr); + + return QueryParameterSetter.lenientQueryParameterSetter(binding.getName(), binding.getPosition(), + binding.prepare(value)); + + } + return null; + } + + private Expression parseExpressionString(String expressionString) { + return parser.parseExpression(expressionString); + } + + private Object evaluateExpression(Expression expr) { + return expr.getValue(getEvaluationContext(), Object.class); + } + + private EvaluationContext getEvaluationContext() { + return evaluationContextProvider.getEvaluationContext(parameters, values); + } + } + + /** + * Extracts values for parameter bindings from method parameters. It handles named as well as indexed parameters. + * + * @author Jens Schauder + * @since 2.0 + */ + class BasicParameterStrategy implements QueryParameterSetterStrategy { + + private final JpaParameters parameters; + private final ParameterAccessor accessor; + + BasicParameterStrategy(JpaParameters parameters, ParameterAccessor accessor) { + + this.parameters = parameters; + this.accessor = accessor; + } + + @Override + public QueryParameterSetter create(Query jpaQuery, ParameterBinding binding) { + + JpaParameter parameter = findParameterForBinding( // + QueryUtils.hasNamedParameter(jpaQuery) // + ? matchByName(binding) : matchByIndex(binding) // + ); + + if (parameter == null) { + return QueryParameterSetter.NOOP; + } + + Object preparedValue = binding.prepare(accessor.getBindableValue(parameter.getIndex())); + + return QueryParameterSetter.fromJpaParameter(parameter, binding.getPosition(), preparedValue); + } + + private JpaParameter findParameterForBinding(Predicate predicate) { + + for (JpaParameter methodParameterCandidate : parameters.getBindableParameters()) { + if (predicate.test(methodParameterCandidate)) { + return methodParameterCandidate; + } + } + + return null; + } + + private static Predicate matchByIndex(ParameterBinding binding) { + return p -> p.getIndex() + 1 == binding.getPosition(); + } + + private static Predicate matchByName(ParameterBinding binding) { + + return p -> binding.getName().equals( // + p.getName().orElseThrow( // + () -> new IllegalArgumentException("Parameter needs to be named!") // + )); + } + + } +} diff --git a/src/main/java/org/springframework/data/jpa/repository/query/SpelExpressionStringQueryParameterBinder.java b/src/main/java/org/springframework/data/jpa/repository/query/SpelExpressionStringQueryParameterBinder.java deleted file mode 100644 index 8f1560d73..000000000 --- a/src/main/java/org/springframework/data/jpa/repository/query/SpelExpressionStringQueryParameterBinder.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright 2014 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 - * - * http://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 java.util.List; - -import javax.persistence.Query; - -import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; -import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding; -import org.springframework.data.repository.query.EvaluationContextProvider; -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.Expression; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.util.Assert; - -/** - * A {@link StringQueryParameterBinder} that is able to bind synthetic query parameters. - * - * @author Thomas Darimont - */ -class SpelExpressionStringQueryParameterBinder extends StringQueryParameterBinder { - - private final StringQuery query; - private final EvaluationContextProvider evaluationContextProvider; - private final SpelExpressionParser parser; - - /** - * Creates a new {@link SpelExpressionStringQueryParameterBinder}. - * - * @param parameters must not be {@literal null} - * @param values must not be {@literal null} - * @param query must not be {@literal null} - * @param evaluationContextProvider must not be {@literal null} - * @param parser must not be {@literal null} - */ - public SpelExpressionStringQueryParameterBinder(JpaParameters parameters, Object[] values, StringQuery query, - EvaluationContextProvider evaluationContextProvider, SpelExpressionParser parser) { - - super(parameters, values, query); - Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!"); - Assert.notNull(parser, "SpelExpressionParser must not be null!"); - - this.evaluationContextProvider = evaluationContextProvider; - this.query = query; - this.parser = parser; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jpa.repository.query.ParameterBinder#bind(javax.persistence.Query) - */ - @Override - public T bind(T jpaQuery) { - return potentiallyBindExpressionParameters(super.bind(jpaQuery)); - } - - /** - * @param jpaQuery must not be {@literal null} - * @return - */ - private T potentiallyBindExpressionParameters(T jpaQuery) { - - if (isJpaParameterInformationReliable(jpaQuery) && jpaQuery.getParameters().isEmpty()) { - // We can rely on the fact there are no parameters in the given query. - return jpaQuery; - } - - for (ParameterBinding binding : query.getParameterBindings()) { - - if (binding.isExpression()) { - - Expression expr = parseExpressionString(binding.getExpression()); - - Object value = evaluateExpression(expr); - - try { - if (binding.getName() != null) { - jpaQuery.setParameter(binding.getName(), binding.prepare(value)); - } else { - jpaQuery.setParameter(binding.getPosition(), binding.prepare(value)); - } - } catch (IllegalArgumentException iae) { - - // Since Eclipse doesn't reliably report whether a query has parameters - // we simply try to set the parameters and ignore possible failures. - } - } - } - - return jpaQuery; - } - - private boolean isJpaParameterInformationReliable(T jpaQuery) { - - String className = jpaQuery.getClass().getName(); - return className.startsWith("org.apache.openjpa") || className.startsWith("org.hibernate"); - } - - /** - * Parses the given {@code expressionString} into a SpEL {@link Expression}. - * - * @param expressionString - * @return - */ - private Expression parseExpressionString(String expressionString) { - return parser.parseExpression(expressionString); - } - - /** - * Evaluates the given SpEL {@link Expression}. - * - * @param expr - * @return - */ - private Object evaluateExpression(Expression expr) { - return expr.getValue(getEvaluationContext(), Object.class); - } - - /** - * Returns the {@link StandardEvaluationContext} to use for evaluation. - * - * @return - */ - private EvaluationContext getEvaluationContext() { - return evaluationContextProvider.getEvaluationContext(getParameters(), getValues()); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jpa.repository.query.ParameterBinder#canBindParameter(org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter) - */ - @Override - protected boolean canBindParameter(JpaParameter parameter) { - - List parameterBindings = query.getParameterBindings(); - - // if no parameter bindings are present, we simply rely on the check in super. - if (parameterBindings.isEmpty()) { - return super.canBindParameter(parameter); - } - - // otherwise determine whether there are any non expression parameters left to be bound. - int expressionParameterCount = 0; - for (ParameterBinding binding : parameterBindings) { - - if (binding.isExpression()) { - expressionParameterCount++; - } - } - - boolean allParametersAreUsedInExpressions = parameterBindings.size() - expressionParameterCount == 0; - - // if all parameters are used in expressions, then we can skip their bindings now, since they'll get bound later. - return !allParametersAreUsedInExpressions && super.canBindParameter(parameter); - } -} diff --git a/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java index 285d41439..9eb641f7f 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java @@ -32,12 +32,19 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** - * Encapsulation of a String JPA query. + * Encapsulation of a JPA query String. + * + * Offers access to parameters as bindings. The internal query String is cleaned from decorated parameters like {@literal %:lastname%} and the matching bindings take care of applying the decorations in the {@link ParameterBinding#prepare(Object)} method. + * + * Note that this class also handles replacing SpEL expressions with synthetic bind parameters + * + * * * @author Oliver Gierke * @author Thomas Darimont * @author Oliver Wehrens * @author Mark Paluch + * @author Jens Schauder */ class StringQuery { diff --git a/src/main/java/org/springframework/data/jpa/repository/query/StringQueryParameterBinder.java b/src/main/java/org/springframework/data/jpa/repository/query/StringQueryParameterBinder.java deleted file mode 100644 index 83084f8e9..000000000 --- a/src/main/java/org/springframework/data/jpa/repository/query/StringQueryParameterBinder.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2013-2017 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 - * - * http://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 javax.persistence.Query; - -import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter; -import org.springframework.data.jpa.repository.query.StringQuery.LikeParameterBinding; -import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding; -import org.springframework.data.repository.query.Parameter; -import org.springframework.data.repository.query.Parameters; -import org.springframework.util.Assert; - -/** - * {@link ParameterBinder} that takes {@link LikeParameterBinding}s encapsulated in a {@link StringQuery} into account. - * - * @author Oliver Gierke - * @author Thomas Darimont - * @author Christoph Strobl - */ -public class StringQueryParameterBinder extends ParameterBinder { - - private final StringQuery query; - - /** - * Creates a new {@link StringQueryParameterBinder} from the given {@link Parameters}, method arguments and - * {@link StringQuery}. - * - * @param parameters must not be {@literal null}. - * @param values must not be {@literal null}. - * @param query must not be {@literal null}. - */ - public StringQueryParameterBinder(JpaParameters parameters, Object[] values, StringQuery query) { - - super(parameters, values); - - Assert.notNull(query, "StringQuery must not be null!"); - this.query = query; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.jpa.repository.query.ParameterBinder#bind(javax.persistence.Query, org.springframework.data.repository.query.Parameter, java.lang.Object, int) - */ - @Override - protected void bind(Query jpaQuery, JpaParameter methodParameter, Object value, int position) { - - ParameterBinding binding = getBindingFor(jpaQuery, position, methodParameter); - super.bind(jpaQuery, methodParameter, binding.prepare(value), position); - } - - /** - * Finds the {@link LikeParameterBinding} to be applied before binding a parameter value to the query. - * - * @param jpaQuery must not be {@literal null}. - * @param position - * @param parameter must not be {@literal null}. - * @return the {@link ParameterBinding} for the given parameters or {@literal null} if none available. - */ - private ParameterBinding getBindingFor(Query jpaQuery, int position, Parameter parameter) { - - Assert.notNull(jpaQuery, "Query must not be null!"); - Assert.notNull(parameter, "Parameter must not be null!"); - - if (hasNamedParameter(jpaQuery)) { - return query.getBindingFor( - parameter.getName().orElseThrow(() -> new IllegalArgumentException("Parameter needs to be named!"))); - } - - try { - - jpaQuery.getParameter(position); - return query.getBindingFor(position); - - } catch (IllegalArgumentException o_O) { - - // We should actually reject parameters unavailable, but as EclipseLink doesn't implement ….getParameter(int) for - // native queries correctly we need to fall back to an indexed parameter - // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=427892 - - return new ParameterBinding(position); - } - } -} diff --git a/src/test/java/org/springframework/data/jpa/repository/query/ParameterBinderUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/ParameterBinderUnitTests.java index a0a66257c..3b6edf5d2 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/ParameterBinderUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/ParameterBinderUnitTests.java @@ -15,10 +15,12 @@ */ package org.springframework.data.jpa.repository.query; +import static java.util.Collections.*; import static javax.persistence.TemporalType.*; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.anyObject; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import java.lang.reflect.Method; @@ -27,6 +29,7 @@ import java.util.List; import java.util.Optional; import javax.persistence.Embeddable; +import javax.persistence.Parameter; import javax.persistence.Query; import javax.persistence.TemporalType; @@ -45,6 +48,7 @@ import org.springframework.data.repository.query.Param; * * @author Oliver Gierke * @author Thomas Darimont + * @author Jens Schauder */ @RunWith(MockitoJUnitRunner.class) public class ParameterBinderUnitTests { @@ -150,14 +154,13 @@ public class ParameterBinderUnitTests { public void usesParameterNameIfAnnotated() throws Exception { when(query.setParameter(eq("username"), anyObject())).thenReturn(query); - new ParameterBinder(new JpaParameters(valid), new Object[] { "foo" }) { - @Override - boolean hasNamedParameter(Query query) { + Parameter parameter = mock(Parameter.class); + when(parameter.getName()).thenReturn("username"); + when(query.getParameters()).thenReturn(singleton(parameter)); + + new ParameterBinder(new JpaParameters(valid), new Object[] { "foo" }).bind(query); - return true; - } - }.bind(query); verify(query).setParameter(eq("username"), anyObject()); }