ParameterBinding.java
/*
* Copyright 2023-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.util.ObjectUtils.*;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Score;
import org.springframework.data.domain.Vector;
import org.springframework.data.expression.ValueExpression;
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.parser.Part;
import org.springframework.data.repository.query.parser.Part.Type;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* A generic parameter binding with name or position information.
*
* @author Thomas Darimont
* @author Mark Paluch
* @author Christoph Strobl
*/
public class ParameterBinding {
private final BindingIdentifier identifier;
private final ParameterOrigin origin;
/**
* 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)
*/
ParameterBinding(BindingIdentifier identifier, ParameterOrigin origin) {
Assert.notNull(identifier, "BindingIdentifier must not be null");
Assert.notNull(origin, "ParameterOrigin must not be null");
this.identifier = identifier;
this.origin = origin;
}
public BindingIdentifier getIdentifier() {
return identifier;
}
public ParameterOrigin getOrigin() {
return origin;
}
/**
* @return the name if available or {@literal null}.
*/
public @Nullable String getName() {
return identifier.hasName() ? identifier.getName() : null;
}
/**
* @return {@literal true} if the binding identifier is associated with a name.
* @since 4.0
*/
boolean hasName() {
return identifier.hasName();
}
/**
* @return the name
* @throws IllegalStateException if the name is not available.
* @since 2.0
*/
String getRequiredName() throws IllegalStateException {
String name = getName();
if (name != null) {
return name;
}
throw new IllegalStateException(String.format("Required name for %s not available", this));
}
/**
* @return the position if available or {@literal null}.
*/
@Nullable
Integer getPosition() {
return identifier.hasPosition() ? identifier.getPosition() : null;
}
/**
* @return the position
* @throws IllegalStateException if the position is not available.
* @since 2.0
*/
int getRequiredPosition() throws IllegalStateException {
Integer position = getPosition();
if (position != null) {
return position;
}
throw new IllegalStateException(String.format("Required position for %s not available", this));
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
ParameterBinding that = (ParameterBinding) o;
if (!nullSafeEquals(identifier, that.identifier)) {
return false;
}
return nullSafeEquals(origin, that.origin);
}
@Override
public int hashCode() {
int result = nullSafeHashCode(identifier);
result = 31 * result + nullSafeHashCode(origin);
return result;
}
@Override
public String toString() {
return String.format("ParameterBinding [identifier: %s, origin: %s]", identifier, origin);
}
/**
* @param valueToBind value to prepare
*/
public @Nullable Object prepare(@Nullable Object valueToBind) {
if (valueToBind instanceof Score score) {
return score.getValue();
}
if (valueToBind instanceof Vector v) {
return v.getType() == Float.TYPE ? v.toFloatArray() : v.toDoubleArray();
}
return valueToBind;
}
/**
* Check whether the {@code other} binding uses the same bind target.
*
* @param other must not be {@literal null}.
* @return {@code true} if the other binding uses the same parameter to bind to as this one.
*/
public boolean bindsTo(ParameterBinding other) {
if (getIdentifier().equals(other.getIdentifier())) {
return true;
}
if (identifier.hasName() && other.identifier.hasName()) {
if (identifier.getName().equals(other.identifier.getName())) {
return true;
}
}
if (identifier.hasPosition() && other.identifier.hasPosition()) {
if (identifier.getPosition() == other.identifier.getPosition()) {
return true;
}
}
return false;
}
/**
* Check whether this binding can be bound as the {@code other} binding by checking its type and origin. Subclasses
* may override this method to include other properties for the compatibility check.
*
* @param other
* @return {@code true} if the other binding is compatible with this one.
*/
public boolean isCompatibleWith(ParameterBinding other) {
return other.getClass() == getClass() && other.getOrigin().equals(getOrigin());
}
/**
* Represents a {@link ParameterBinding} in a JPQL query augmented with instructions of how to apply a parameter as an
* {@code IN} parameter.
*
* @author Thomas Darimont
* @author Mark Paluch
*/
public static class PartTreeParameterBinding extends ParameterBinding {
private final Class<?> parameterType;
private final JpqlQueryTemplates templates;
private final EscapeCharacter escape;
private final Type type;
private final boolean ignoreCase;
private final boolean noWildcards;
private final @Nullable Object value;
public PartTreeParameterBinding(BindingIdentifier identifier, ParameterOrigin origin, Class<?> parameterType,
Part part, @Nullable Object value, JpqlQueryTemplates templates, EscapeCharacter escape) {
super(identifier, origin);
this.parameterType = parameterType;
this.templates = templates;
this.escape = escape;
this.value = value;
this.type = value == null
&& (Type.SIMPLE_PROPERTY.equals(part.getType()) || Type.NEGATING_SIMPLE_PROPERTY.equals(part.getType()))
? Type.IS_NULL
: part.getType();
this.ignoreCase = Part.IgnoreCaseType.ALWAYS.equals(part.shouldIgnoreCase());
this.noWildcards = part.getProperty().getLeafProperty().isCollection();
}
/**
* Returns whether the parameter shall be considered an {@literal IS NULL} parameter.
*/
public boolean isIsNullParameter() {
return Type.IS_NULL.equals(type);
}
public boolean isIgnoreCase() {
return ignoreCase;
}
public JpqlQueryTemplates getTemplates() {
return templates;
}
public @Nullable Object getValue() {
return value;
}
@Override
public @Nullable Object prepare(@Nullable Object value) {
value = super.prepare(value);
if (value == null || parameterType == null) {
return value;
}
if (String.class.equals(parameterType) && !noWildcards) {
return switch (type) {
case STARTING_WITH -> String.format("%s%%", escape.escape(value.toString()));
case ENDING_WITH -> String.format("%%%s", escape.escape(value.toString()));
case CONTAINING, NOT_CONTAINING -> String.format("%%%s%%", escape.escape(value.toString()));
default -> value;
};
}
return Collection.class.isAssignableFrom(parameterType) //
? potentiallyIgnoreCase(ignoreCase, toCollection(value)) //
: value;
}
@SuppressWarnings("unchecked")
@Contract("false, _ -> param2; _, null -> null; true, !null -> new")
private @Nullable Collection<?> potentiallyIgnoreCase(boolean ignoreCase, @Nullable Collection<?> collection) {
if (!ignoreCase || CollectionUtils.isEmpty(collection)) {
return collection;
}
return ((Collection<String>) collection).stream() //
.map(it -> it == null //
? null //
: templates.ignoreCase(it)) //
.collect(Collectors.toList());
}
/**
* Returns the given argument as {@link Collection} which means it will return it as is if it's a
* {@link Collections}, turn an array into an {@link ArrayList} or simply wrap any other value into a single element
* {@link Collections}.
*
* @param value the value to be converted to a {@link Collection}.
* @return the object itself as a {@link Collection} or a {@link Collection} constructed from the value.
*/
private static @Nullable Collection<?> toCollection(@Nullable Object value) {
if (value == null) {
return null;
}
if (value instanceof Collection<?> collection) {
return collection.isEmpty() ? null : collection;
}
if (ObjectUtils.isArray(value)) {
List<Object> collection = Arrays.asList(ObjectUtils.toObjectArray(value));
return collection.isEmpty() ? null : collection;
}
return Collections.singleton(value);
}
}
/**
* Represents a {@link ParameterBinding} in a JPQL query augmented with instructions of how to apply a parameter as an
* {@code IN} parameter.
*
* @author Thomas Darimont
*/
static class InParameterBinding extends ParameterBinding {
/**
* Creates a new {@link InParameterBinding} for the parameter with the given name.
*/
InParameterBinding(BindingIdentifier identifier, ParameterOrigin origin) {
super(identifier, origin);
}
@Override
public @Nullable Object prepare(@Nullable Object value) {
if (!ObjectUtils.isArray(value)) {
return value;
}
int length = Array.getLength(value);
Collection<Object> result = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
result.add(Array.get(value, i));
}
return result;
}
}
/**
* Represents a parameter binding in a JPQL query augmented with instructions of how to apply a parameter as LIKE
* parameter. This allows expressions like {@code ���like %?1} in the JPQL query, which is not allowed by plain JPA.
*
* @author Oliver Gierke
* @author Thomas Darimont
*/
static class LikeParameterBinding extends ParameterBinding {
private static final List<Type> SUPPORTED_TYPES = Arrays.asList(Type.CONTAINING, Type.STARTING_WITH,
Type.ENDING_WITH, Type.LIKE);
private final Type type;
/**
* Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type} and parameter
* binding input.
*
* @param identifier must not be {@literal null} or empty.
* @param type must not be {@literal null}.
*/
LikeParameterBinding(BindingIdentifier identifier, ParameterOrigin origin, Type type) {
super(identifier, origin);
Assert.notNull(type, "Type must not be null");
Assert.isTrue(SUPPORTED_TYPES.contains(type),
String.format("Type must be one of %s", StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES)));
this.type = type;
}
/**
* Returns the {@link Type} of the binding.
*
* @return the type
*/
public Type getType() {
return type;
}
/**
* Extracts the raw value properly.
*/
@Override
public @Nullable Object prepare(@Nullable Object value) {
Object unwrapped = PersistenceProvider.unwrapTypedParameterValue(super.prepare(value));
if (unwrapped == null) {
return null;
}
return switch (type) {
case STARTING_WITH -> String.format("%s%%", unwrapped);
case ENDING_WITH -> String.format("%%%s", unwrapped);
case CONTAINING -> String.format("%%%s%%", unwrapped);
default -> unwrapped;
};
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof LikeParameterBinding that)) {
return false;
}
return super.equals(obj) && this.type.equals(that.type);
}
@Override
public int hashCode() {
int result = super.hashCode();
result += nullSafeHashCode(this.type);
return result;
}
@Override
public String toString() {
return String.format("LikeBinding [identifier: %s, origin: %s, type: %s]", getIdentifier(), getOrigin(),
getType());
}
@Override
public boolean isCompatibleWith(ParameterBinding binding) {
if (super.isCompatibleWith(binding) && binding instanceof LikeParameterBinding other) {
return getType() == other.getType();
}
return false;
}
/**
* Extracts the like {@link Type} from the given JPA like expression.
*
* @param expression must not be {@literal null} or empty.
*/
static Type getLikeTypeFrom(String expression) {
Assert.hasText(expression, "Expression must not be null or empty");
if (expression.startsWith("%")) {
return expression.endsWith("%") ? Type.CONTAINING : Type.ENDING_WITH;
}
if (expression.endsWith("%")) {
return Type.STARTING_WITH;
}
return Type.LIKE;
}
}
/**
* Identifies a binding parameter by name, position or both. Used to bind parameters to a query or to describe a
* {@link MethodInvocationArgument} origin.
*
* @author Mark Paluch
* @since 3.1.2
*/
public sealed interface BindingIdentifier permits Named, Indexed, NamedAndIndexed {
/**
* Creates an identifier for the given {@code name}.
*
* @param name
* @return
*/
static BindingIdentifier of(String name) {
Assert.hasText(name, "Name must not be empty");
return new Named(name);
}
/**
* Creates an identifier for the given {@code position}.
*
* @param position 1-based index.
* @return
*/
static BindingIdentifier of(int position) {
Assert.isTrue(position > 0, "Index position must be greater zero");
return new Indexed(position);
}
/**
* Creates an identifier for the given {@code name} and {@code position}.
*
* @param name
* @return
*/
static BindingIdentifier of(String name, int position) {
Assert.hasText(name, "Name must not be empty");
return new NamedAndIndexed(name, position);
}
/**
* @return {@code true} if the binding is associated with a name.
*/
default boolean hasName() {
return false;
}
/**
* @return {@code true} if the binding is associated with a position index.
*/
default boolean hasPosition() {
return false;
}
/**
* Returns the binding name {@link #hasName() if present} or throw {@link IllegalStateException} if no name
* associated.
*
* @return the binding name.
*/
default String getName() {
throw new IllegalStateException("No name associated");
}
/**
* Returns the binding name {@link #hasPosition() if present} or throw {@link IllegalStateException} if no position
* associated.
*
* @return the binding position.
*/
default int getPosition() {
throw new IllegalStateException("No position associated");
}
/**
* Map the name of the binding to a new name using the given {@link Function} if the binding has a name. If the
* binding is not associated with a name, then the binding is returned unchanged.
*
* @param nameMapper must not be {@literal null}.
* @return the transformed {@link BindingIdentifier} if the binding has a name, otherwise the binding itself.
* @since 4.0
*/
BindingIdentifier mapName(Function<? super String, ? extends String> nameMapper);
/**
* Associate a position with the binding.
*
* @param position
* @return the new binding identifier with the position.
* @since 4.0
*/
BindingIdentifier withPosition(int position);
}
private record Named(String name) implements BindingIdentifier {
@Override
public boolean hasName() {
return true;
}
@Override
public String getName() {
return name();
}
@Override
public String toString() {
return name();
}
@Override
public BindingIdentifier mapName(Function<? super String, ? extends String> nameMapper) {
return new Named(nameMapper.apply(name()));
}
@Override
public BindingIdentifier withPosition(int position) {
return new NamedAndIndexed(name, position);
}
}
private record Indexed(int position) implements BindingIdentifier {
@Override
public boolean hasPosition() {
return true;
}
@Override
public int getPosition() {
return position();
}
@Override
public BindingIdentifier mapName(Function<? super String, ? extends String> nameMapper) {
return this;
}
@Override
public BindingIdentifier withPosition(int position) {
return new Indexed(position);
}
@Override
public String toString() {
return "[" + position() + "]";
}
}
private record NamedAndIndexed(String name, int position) implements BindingIdentifier {
@Override
public boolean hasName() {
return true;
}
@Override
public String getName() {
return name();
}
@Override
public boolean hasPosition() {
return true;
}
@Override
public int getPosition() {
return position();
}
@Override
public BindingIdentifier mapName(Function<? super String, ? extends String> nameMapper) {
return new NamedAndIndexed(nameMapper.apply(name), position);
}
@Override
public BindingIdentifier withPosition(int position) {
return new NamedAndIndexed(name, position);
}
@Override
public String toString() {
return "[" + name() + ", " + position() + "]";
}
}
/**
* Value type hierarchy to describe where a binding parameter comes from, either method call or an expression.
*
* @author Mark Paluch
* @since 3.1.2
*/
public sealed interface ParameterOrigin permits Expression, MethodInvocationArgument, Synthetic {
/**
* Creates a {@link Expression} for the given {@code expression}.
*
* @param expression must not be {@literal null}.
* @return {@link Expression} for the given {@code expression}.
*/
static Expression ofExpression(ValueExpression expression) {
return new Expression(expression);
}
/**
* Creates a {@link Expression} for the given {@code expression} string.
*
* @param value the captured value.
* @param source source from which this value is derived.
* @return {@link Synthetic} for the given {@code value}.
*/
static Synthetic synthetic(@Nullable Object value, Object source) {
return new Synthetic(value, source);
}
/**
* Creates a {@link MethodInvocationArgument} object for {@code name}
*
* @param name the parameter name from the method invocation.
* @return {@link MethodInvocationArgument} object for {@code name}.
*/
static MethodInvocationArgument ofParameter(String name) {
return ofParameter(name, null);
}
/**
* Creates a {@link MethodInvocationArgument} object for {@code name} and {@code position}. Either the name or the
* position must be given.
*
* @param name the parameter name from the method invocation, can be {@literal null}.
* @param position the parameter position (1-based) from the method invocation, can be {@literal null}.
* @return {@link MethodInvocationArgument} object for {@code name} and {@code position}.
*/
static MethodInvocationArgument ofParameter(@Nullable String name, @Nullable Integer position) {
BindingIdentifier identifier;
if (!ObjectUtils.isEmpty(name) && position != null) {
identifier = BindingIdentifier.of(name, position);
} else if (!ObjectUtils.isEmpty(name)) {
identifier = BindingIdentifier.of(name);
} else if (position != null) {
identifier = BindingIdentifier.of(position);
} else {
throw new IllegalStateException("Neither name nor position available for binding");
}
return ofParameter(identifier);
}
/**
* Creates a {@link MethodInvocationArgument} object for {@code position}.
*
* @param parameter the parameter from the method invocation.
* @return {@link MethodInvocationArgument} object for {@code position}.
*/
static MethodInvocationArgument ofParameter(Parameter parameter) {
return ofParameter(parameter.getIndex() + 1);
}
/**
* Creates a {@link MethodInvocationArgument} object for {@code position}.
*
* @param position the parameter position (1-based) from the method invocation.
* @return {@link MethodInvocationArgument} object for {@code position}.
*/
static MethodInvocationArgument ofParameter(int position) {
return ofParameter(BindingIdentifier.of(position));
}
/**
* Creates a {@link MethodInvocationArgument} using {@link BindingIdentifier}.
*
* @param identifier must not be {@literal null}.
* @return {@link MethodInvocationArgument} for {@link BindingIdentifier}.
*/
static MethodInvocationArgument ofParameter(BindingIdentifier identifier) {
return new MethodInvocationArgument(identifier);
}
/**
* @return {@code true} if the origin is a method argument reference.
*/
boolean isMethodArgument();
/**
* @return {@code true} if the origin is an expression.
*/
boolean isExpression();
/**
* @return {@code true} if the origin is synthetic (contributed by e.g. KeysetPagination)
*/
boolean isSynthetic();
}
/**
* Value object capturing the expression of which a binding parameter originates.
*
* @param expression
* @author Mark Paluch
* @since 3.1.2
*/
public record Expression(ValueExpression expression) implements ParameterOrigin {
@Override
public boolean isMethodArgument() {
return false;
}
@Override
public boolean isExpression() {
return true;
}
@Override
public boolean isSynthetic() {
return true;
}
}
/**
* Value object capturing the expression of which a binding parameter originates.
*
* @param value
* @param source
* @author Mark Paluch
*/
public record Synthetic(@Nullable Object value, Object source) implements ParameterOrigin {
@Override
public boolean isMethodArgument() {
return false;
}
@Override
public boolean isExpression() {
return false;
}
@Override
public boolean isSynthetic() {
return true;
}
}
/**
* Value object capturing the method invocation parameter reference.
*
* @param identifier
* @author Mark Paluch
* @since 3.1.2
*/
public record MethodInvocationArgument(BindingIdentifier identifier) implements ParameterOrigin {
@Override
public boolean isMethodArgument() {
return true;
}
@Override
public boolean isExpression() {
return false;
}
@Override
public boolean isSynthetic() {
return false;
}
}
}