ParameterMetadataProvider.java
/*
* Copyright 2011-2025 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 static org.springframework.data.jpa.repository.query.ParameterBinding.*;
import jakarta.persistence.criteria.CriteriaBuilder;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.Score;
import org.springframework.data.domain.ScoringFunction;
import org.springframework.data.domain.Vector;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.support.JpqlQueryTemplates;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.expression.Expression;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Helper class to allow easy creation of {@link PartTreeParameterBinding}s.
*
* @author Oliver Gierke
* @author Thomas Darimont
* @author Mark Paluch
* @author Christoph Strobl
* @author Jens Schauder
* @author Andrey Kovalev
* @author Yuriy Tsarkov
* @author Donghun Shin
* @author Greg Turnquist
*/
public class ParameterMetadataProvider {
static final Object PLACEHOLDER = new Object();
private final Iterator<? extends Parameter> parameters;
private final @Nullable JpaParametersParameterAccessor accessor;
private final List<ParameterBinding> bindings;
private final Set<String> syntheticParameterNames = new LinkedHashSet<>();
private @Nullable ParameterBinding vector;
private final @Nullable Iterator<Object> bindableParameterValues;
private final EscapeCharacter escape;
private final JpqlQueryTemplates templates;
private final JpaParameters jpaParameters;
private int position;
private int bindMarker;
/**
* Creates a new {@link ParameterMetadataProvider} from the given {@link CriteriaBuilder} and
* {@link ParametersParameterAccessor}.
*
* @param accessor must not be {@literal null}.
* @param escape must not be {@literal null}.
* @param templates must not be {@literal null}.
*/
public ParameterMetadataProvider(JpaParametersParameterAccessor accessor, EscapeCharacter escape,
JpqlQueryTemplates templates) {
this(accessor.iterator(), accessor, accessor.getParameters(), escape, templates);
}
/**
* Creates a new {@link ParameterMetadataProvider} from the given {@link CriteriaBuilder} and {@link Parameters} with
* support for parameter value customizations via {@link PersistenceProvider}.
*
* @param parameters must not be {@literal null}.
* @param escape must not be {@literal null}.
* @param templates must not be {@literal null}.
*/
public ParameterMetadataProvider(JpaParameters parameters, EscapeCharacter escape, JpqlQueryTemplates templates) {
this(null, null, parameters, escape, templates);
}
/**
* Creates a new {@link ParameterMetadataProvider} from the given {@link CriteriaBuilder} an {@link Iterable} of all
* bindable parameter values, and {@link Parameters}.
*
* @param bindableParameterValues may be {@literal null}.
* @param parameters must not be {@literal null}.
* @param escape must not be {@literal null}.
* @param templates must not be {@literal null}.
*/
private ParameterMetadataProvider(@Nullable Iterator<Object> bindableParameterValues,
@Nullable JpaParametersParameterAccessor accessor, JpaParameters parameters, EscapeCharacter escape,
JpqlQueryTemplates templates) {
Assert.notNull(parameters, "Parameters must not be null");
Assert.notNull(escape, "EscapeCharacter must not be null");
Assert.notNull(templates, "JpqlQueryTemplates must not be null");
this.jpaParameters = parameters;
this.accessor = accessor;
this.parameters = parameters.getBindableParameters().iterator();
this.bindings = new ArrayList<>();
this.bindableParameterValues = bindableParameterValues;
this.escape = escape;
this.templates = templates;
}
JpaParameters getParameters() {
return this.jpaParameters;
}
/**
* Returns all {@link ParameterBinding}s built.
*
* @return the bindings.
*/
public List<ParameterBinding> getBindings() {
return bindings;
}
/**
* @return the {@link SimilarityNormalizer}.
*/
SimilarityNormalizer getSimilarityNormalizer() {
if (accessor != null && accessor.normalizeSimilarity()) {
return SimilarityNormalizer.get(accessor.getScoringFunction());
}
return SimilarityNormalizer.IDENTITY;
}
/**
* Builds a new {@link PartTreeParameterBinding} for given {@link Part} and the next {@link Parameter}.
*/
@SuppressWarnings("unchecked")
PartTreeParameterBinding next(Part part) {
Assert.isTrue(parameters.hasNext(), () -> String.format("No parameter available for part %s", part));
Parameter parameter = parameters.next();
return next(part, parameter.getType(), parameter);
}
/**
* Builds a new {@link PartTreeParameterBinding} of the given {@link Part} and type. Forwards the underlying
* {@link Parameters} as well.
*
* @param <T> is the type parameter of the returned {@link PartTreeParameterBinding}.
* @param type must not be {@literal null}.
* @return ParameterMetadata for the next parameter.
*/
<T> PartTreeParameterBinding next(Part part, Class<T> type) {
Parameter parameter = parameters.next();
Class<?> typeToUse = ClassUtils.isAssignable(type, parameter.getType()) ? parameter.getType() : type;
return next(part, typeToUse, parameter);
}
/**
* Builds a new {@link PartTreeParameterBinding} for the given type and name.
*
* @param <T> type parameter for the returned {@link PartTreeParameterBinding}.
* @param part must not be {@literal null}.
* @param type must not be {@literal null}.
* @param parameter providing the name for the returned {@link PartTreeParameterBinding}.
* @return a new {@link PartTreeParameterBinding} for the given type and name.
*/
private <T> PartTreeParameterBinding next(Part part, Class<T> type, Parameter parameter) {
Assert.notNull(type, "Type must not be null");
/*
* We treat Expression types as Object vales since the real value to be bound as a parameter is determined at query time.
*/
@SuppressWarnings("unchecked")
Class<T> reifiedType = Expression.class.equals(type) ? (Class<T>) Object.class : type;
Object value = bindableParameterValues == null ? PLACEHOLDER : bindableParameterValues.next();
int currentPosition = ++position;
int currentBindMarker = ++bindMarker;
BindingIdentifier bindingIdentifier = parameter.getName().map(it -> BindingIdentifier.of(it, currentBindMarker))
.orElseGet(() -> BindingIdentifier.of(currentBindMarker));
BindingIdentifier origin = parameter.getName().map(it -> BindingIdentifier.of(it, currentPosition))
.orElseGet(() -> BindingIdentifier.of(currentPosition));
/* identifier refers to bindable parameters, not _all_ parameters index */
MethodInvocationArgument methodParameter = ParameterOrigin.ofParameter(origin);
PartTreeParameterBinding binding = new PartTreeParameterBinding(bindingIdentifier,
methodParameter, reifiedType, part, value, templates, escape);
// PartTreeParameterBinding is more expressive than a potential ParameterBinding for Vector.
bindings.add(binding);
if (Vector.class.isAssignableFrom(parameter.getType())) {
this.vector = binding;
}
return binding;
}
/**
* @return the scoring function if available {@link ScoringFunction#unspecified()} by default.
* @since 4.0
*/
ScoringFunction getScoringFunction() {
if (accessor != null) {
return accessor.getScoringFunction();
}
return ScoringFunction.unspecified();
}
/**
*
* @return the vector binding identifier.
* @throws IllegalStateException if parameters do not cotain
* @since 4.0
*/
ParameterBinding getVectorBinding() {
if (!getParameters().hasVectorParameter()) {
throw new IllegalStateException("Vector parameter not available");
}
if (this.vector != null) {
return this.vector;
}
int vectorIndex = getParameters().getVectorIndex();
BindingIdentifier bindingIdentifier = BindingIdentifier.of(vectorIndex + 1);
/* identifier refers to bindable parameters, not _all_ parameters index */
MethodInvocationArgument methodParameter = ParameterOrigin.ofParameter(bindingIdentifier);
ParameterBinding parameterBinding = new ParameterBinding(bindingIdentifier, methodParameter);
this.bindings.add(parameterBinding);
return parameterBinding;
}
EscapeCharacter getEscape() {
return escape;
}
/**
* Builds a new synthetic {@link ParameterBinding} for the given value.
*
* @param nameHint
* @param value
* @param source
* @return a new {@link ParameterBinding} for the given value and source.
*/
ParameterBinding nextSynthetic(String nameHint, Object value, Object source) {
int currentPosition = ++bindMarker;
String bindingName = nameHint;
if (!syntheticParameterNames.add(bindingName)) {
bindingName = bindingName + "_" + currentPosition;
syntheticParameterNames.add(bindingName);
}
return new ParameterBinding(BindingIdentifier.of(bindingName, currentPosition),
ParameterOrigin.synthetic(value, source));
}
RangeParameterBinding lower(PartTreeParameterBinding within, SimilarityNormalizer normalizer) {
int bindMarker = within.getRequiredPosition();
if (!bindings.remove(within)) {
bindMarker = ++this.bindMarker;
}
BindingIdentifier identifier = within.getIdentifier();
RangeParameterBinding rangeBinding = new RangeParameterBinding(
identifier.mapName(name -> name + "_lower").withPosition(bindMarker), within.getOrigin(), true, normalizer);
bindings.add(rangeBinding);
return rangeBinding;
}
RangeParameterBinding upper(PartTreeParameterBinding within, SimilarityNormalizer normalizer) {
int bindMarker = within.getRequiredPosition();
if (!bindings.remove(within)) {
bindMarker = ++this.bindMarker;
}
BindingIdentifier identifier = within.getIdentifier();
RangeParameterBinding rangeBinding = new RangeParameterBinding(
identifier.mapName(name -> name + "_upper").withPosition(bindMarker), within.getOrigin(), false, normalizer);
bindings.add(rangeBinding);
return rangeBinding;
}
ScoreParameterBinding normalize(PartTreeParameterBinding within, SimilarityNormalizer normalizer) {
bindings.remove(within);
ScoreParameterBinding rangeBinding = new ScoreParameterBinding(within.getIdentifier(), within.getOrigin(),
normalizer);
bindings.add(rangeBinding);
return rangeBinding;
}
static class ScoreParameterBinding extends ParameterBinding {
private final SimilarityNormalizer normalizer;
/**
* Creates a new {@link ParameterBinding} for the parameter with the given identifier and origin.
*
* @param identifier of the parameter, must not be {@literal null}.
* @param origin the origin of the parameter (expression or method argument)
*/
ScoreParameterBinding(BindingIdentifier identifier, ParameterOrigin origin, SimilarityNormalizer normalizer) {
super(identifier, origin);
this.normalizer = normalizer;
}
@Override
public @Nullable Object prepare(@Nullable Object valueToBind) {
if (valueToBind instanceof Score score) {
return normalizer.getScore(score.getValue());
}
return super.prepare(valueToBind);
}
@Override
public boolean isCompatibleWith(ParameterBinding binding) {
if (super.isCompatibleWith(binding) && binding instanceof ScoreParameterBinding other) {
return normalizer == other.normalizer;
}
return false;
}
}
static class RangeParameterBinding extends ScoreParameterBinding {
private final boolean lower;
/**
* Creates a new {@link ParameterBinding} for the parameter with the given identifier and origin.
*
* @param identifier of the parameter, must not be {@literal null}.
* @param origin the origin of the parameter (expression or method argument)
*/
RangeParameterBinding(BindingIdentifier identifier, ParameterOrigin origin, boolean lower,
SimilarityNormalizer normalizer) {
super(identifier, origin, normalizer);
this.lower = lower;
}
@Override
public @Nullable Object prepare(@Nullable Object valueToBind) {
if (valueToBind instanceof Range<?> r) {
if (lower) {
return super.prepare(r.getLowerBound().getValue().orElse(null));
} else {
return super.prepare(r.getUpperBound().getValue().orElse(null));
}
}
return super.prepare(valueToBind);
}
@Override
public boolean isCompatibleWith(ParameterBinding binding) {
if (super.isCompatibleWith(binding) && binding instanceof RangeParameterBinding other) {
return lower == other.lower;
}
return false;
}
}
}