org.springframework.data.jpa.repository.query.StringQuery Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of spring-data-jpa Show documentation
Show all versions of spring-data-jpa Show documentation
Spring Data module for JPA repositories.
/*
* Copyright 2013-2015 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 static java.util.regex.Pattern.*;
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.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.data.repository.query.parser.Part.Type;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Encapsulation of a String JPA query.
*
* @author Oliver Gierke
* @author Thomas Darimont
* @author Oliver Wehrens
*/
class StringQuery {
private static final String PARAMETER_NAME_MISSING = "Name for parameter binding must not be null or empty! For named parameters you need to use @Param for query method parameters on Java versions < 8.";
private final String query;
private final List bindings;
private final String alias;
/**
* Creates a new {@link StringQuery} from the given JPQL query.
*
* @param query must not be {@literal null} or empty.
*/
public StringQuery(String query) {
Assert.hasText(query, "Query must not be null or empty!");
this.bindings = new ArrayList();
this.query = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query,
this.bindings);
this.alias = QueryUtils.detectAlias(query);
}
/**
* Returns whether we have found some like bindings.
*
* @return
*/
public boolean hasParameterBindings() {
return !bindings.isEmpty();
}
/**
* Returns the {@link ParameterBinding}s registered.
*
* @return
*/
List getParameterBindings() {
return bindings;
}
/**
* Returns the query string.
*
* @return
*/
public String getQueryString() {
return query;
}
/**
* Returns the main alias used in the query.
*
* @return the alias
*/
public String getAlias() {
return alias;
}
/**
* Returns the {@link ParameterBinding} for the given name.
*
* @param name must not be {@literal null} or empty.
* @return
*/
public ParameterBinding getBindingFor(String name) {
Assert.hasText(name, PARAMETER_NAME_MISSING);
for (ParameterBinding binding : bindings) {
if (binding.hasName(name)) {
return binding;
}
}
throw new IllegalArgumentException(String.format("No parameter binding found for name %s!", name));
}
/**
* Returns the {@link ParameterBinding} for the given position.
*
* @param position
* @return
*/
public ParameterBinding getBindingFor(int position) {
for (ParameterBinding binding : bindings) {
if (binding.hasPosition(position)) {
return binding;
}
}
throw new IllegalArgumentException(String.format("No parameter binding found for position %s!", position));
}
/**
* A parser that extracts the parameter bindings from a given query string.
*
* @author Thomas Darimont
*/
public static enum ParameterBindingParser {
INSTANCE;
static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__";
private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile("\\?(\\d+)");
private static final Pattern PARAMETER_BINDING_PATTERN;
private static final String MESSAGE = "Already found parameter binding with same index / parameter name but differing binding type! "
+ "Already have: %s, found %s! If you bind a parameter multiple times make sure they use the same binding.";
static {
List keywords = new ArrayList();
for (ParameterBindingType type : ParameterBindingType.values()) {
if (type.getKeyword() != null) {
keywords.add(type.getKeyword());
}
}
StringBuilder builder = new StringBuilder();
builder.append("(");
builder.append(StringUtils.collectionToDelimitedString(keywords, "|")); // keywords
builder.append(")?");
builder.append("(?: )?"); // some whitespace
builder.append("\\(?"); // optional braces around parameters
builder.append("(");
builder.append("%?(\\?(\\d+))%?"); // position parameter and parameter index
builder.append("|"); // or
builder.append("%?(:([\\p{L}\\w]+))%?"); // named parameter and the parameter name
builder.append("|"); // or
builder.append("%?((:|\\?)#\\{([^}]+)\\})%?"); // expression parameter and expression
builder.append(")");
builder.append("\\)?"); // optional braces around parameters
PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE);
}
/**
* Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns
* the cleaned up query.
*
* @param query
* @return
*/
private final String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query,
List bindings) {
String result = query;
Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(query);
int greatestParameterIndex = tryFindGreatestParameterIndexIn(query);
boolean parametersShouldBeAccessedByIndex = greatestParameterIndex != -1;
/*
* Prefer indexed access over named parameters if only SpEL Expression parameters are present.
*/
if (!parametersShouldBeAccessedByIndex && query.contains("?#{")) {
parametersShouldBeAccessedByIndex = true;
greatestParameterIndex = 0;
}
/*
* If parameters need to be bound by index, we bind the synthetic expression parameters starting from position of the greatest discovered index parameter in order to
* not mix-up with the actual parameter indices.
*/
int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;
while (matcher.find()) {
String parameterIndexString = matcher.group(4);
String parameterName = parameterIndexString != null ? null : matcher.group(6);
Integer parameterIndex = parameterIndexString == null ? null : Integer.valueOf(parameterIndexString);
String typeSource = matcher.group(1);
String expression = null;
String replacement = null;
if (parameterName == null && parameterIndex == null) {
expressionParameterIndex++;
if (parametersShouldBeAccessedByIndex) {
parameterIndex = expressionParameterIndex;
replacement = "?" + parameterIndex;
} else {
parameterName = EXPRESSION_PARAMETER_PREFIX + expressionParameterIndex;
replacement = ":" + parameterName;
}
expression = matcher.group(9);
}
switch (ParameterBindingType.of(typeSource)) {
case LIKE:
Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
replacement = replacement != null ? replacement : matcher.group(3);
if (parameterIndex != null) {
checkAndRegister(new LikeParameterBinding(parameterIndex, likeType, expression), bindings);
} else {
checkAndRegister(new LikeParameterBinding(parameterName, likeType, expression), bindings);
replacement = expression != null ? ":" + parameterName : matcher.group(5);
}
break;
case IN:
if (parameterIndex != null) {
checkAndRegister(new InParameterBinding(parameterIndex, expression), bindings);
} else {
checkAndRegister(new InParameterBinding(parameterName, expression), bindings);
}
break;
case AS_IS: // fall-through we don't need a special parameter binding for the given parameter.
default:
bindings.add(parameterIndex != null ? new ParameterBinding(null, parameterIndex, expression)
: new ParameterBinding(parameterName, null, expression));
}
if (replacement != null) {
result = StringUtils.replace(result, matcher.group(2), replacement);
}
}
return result;
}
private int tryFindGreatestParameterIndexIn(String query) {
Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query);
int greatestParameterIndex = -1;
while (parameterIndexMatcher.find()) {
String parameterIndexString = parameterIndexMatcher.group(1);
greatestParameterIndex = Math.max(greatestParameterIndex, Integer.parseInt(parameterIndexString));
}
return greatestParameterIndex;
}
private static void checkAndRegister(ParameterBinding binding, List bindings) {
for (ParameterBinding existing : bindings) {
if (existing.hasName(binding.getName()) || existing.hasPosition(binding.getPosition())) {
Assert.isTrue(existing.equals(binding), String.format(MESSAGE, existing, binding));
}
}
if (!bindings.contains(binding)) {
bindings.add(binding);
}
}
/**
* An enum for the different types of bindings.
*
* @author Thomas Darimont
* @author Oliver Gierke
*/
private static enum ParameterBindingType {
// Trailing whitespace is intentional to reflect that the keywords must be used with at least one whitespace
// character, while = does not.
LIKE("like "), IN("in "), AS_IS(null);
private final String keyword;
private ParameterBindingType(String keyword) {
this.keyword = keyword;
}
/**
* Returns the keyword that will tirgger the binding type or {@literal null} if the type is not triggered by a
* keyword.
*
* @return the keyword
*/
public String getKeyword() {
return keyword;
}
/**
* Return the appropriate {@link ParameterBindingType} for the given {@link String}. Returns {@keyword
* #AS_IS} in case no other {@link ParameterBindingType} could be found.
*
* @param typeSource
* @return
*/
static ParameterBindingType of(String typeSource) {
if (!StringUtils.hasText(typeSource)) {
return AS_IS;
}
for (ParameterBindingType type : values()) {
if (type.name().equalsIgnoreCase(typeSource.trim())) {
return type;
}
}
throw new IllegalArgumentException(String.format("Unsupported parameter binding type %s!", typeSource));
}
}
}
/**
* A generic parameter binding with name or position information.
*
* @author Thomas Darimont
*/
static class ParameterBinding {
private final String name;
private final String expression;
private final Integer position;
/**
* Creates a new {@link ParameterBinding} for the parameter with the given name.
*
* @param name must not be {@literal null}.
*/
public ParameterBinding(String name) {
this(name, null, null);
}
/**
* Creates a new {@link ParameterBinding} for the parameter with the given position.
*
* @param position must not be {@literal null}.
*/
public ParameterBinding(Integer position) {
this(null, position, null);
}
/**
* Creates a new {@link ParameterBinding} for the parameter with the given name, position and expression
* information.
*
* @param name
* @param position
* @param expression
*/
ParameterBinding(String name, Integer position, String expression) {
if (name == null) {
Assert.notNull(position, "Position must not be null!");
}
if (position == null) {
Assert.notNull(name, "Name must not be null!");
}
this.name = name;
this.position = position;
this.expression = expression;
}
/**
* Returns whether the binding has the given name. Will always be {@literal false} in case the
* {@link ParameterBinding} has been set up from a position.
*
* @param name
* @return
*/
public boolean hasName(String name) {
return this.position == null && this.name != null && this.name.equals(name);
}
/**
* Returns whether the binding has the given position. Will always be {@literal false} in case the
* {@link ParameterBinding} has been set up from a name.
*
* @param position
* @return
*/
public boolean hasPosition(Integer position) {
return position != null && this.name == null && position.equals(this.position);
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @return the position
*/
public Integer getPosition() {
return position;
}
/**
* @return {@literal true} if this parameter binding is a synthetic SpEL expression.
*/
public boolean isExpression() {
return this.expression != null;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int result = 17;
result += nullSafeHashCode(this.name);
result += nullSafeHashCode(this.position);
result += nullSafeHashCode(this.expression);
return result;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ParameterBinding)) {
return false;
}
ParameterBinding that = (ParameterBinding) obj;
return nullSafeEquals(this.name, that.name) && nullSafeEquals(this.position, that.position)
&& nullSafeEquals(this.expression, that.expression);
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return String.format("ParameterBinding [name: %s, position: %d, expression: %s]", getName(), getPosition(),
getExpression());
}
/**
* @param valueToBind
* @return
*/
public Object prepare(Object valueToBind) {
return valueToBind;
}
public String getExpression() {
return expression;
}
}
/**
* 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.
*
* @param name
* @param expression
*/
public InParameterBinding(String name, String expression) {
super(name, null, expression);
}
/**
* Creates a new {@link InParameterBinding} for the parameter with the given position.
*
* @param position
* @param expression
*/
public InParameterBinding(int position, String expression) {
super(null, position, expression);
}
/*
* (non-Javadoc)
* @see org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding#prepare(java.lang.Object)
*/
@Override
public Object prepare(Object value) {
if (!ObjectUtils.isArray(value)) {
return value;
}
int length = Array.getLength(value);
Collection