Skip to content

DATAJPA-1267 - Using SpEL infrastructure from commons. #250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>2.1.0.BUILD-SNAPSHOT</version>
<version>2.1.0.DATAJPA-1267-SNAPSHOT</version>

<name>Spring Data JPA</name>
<description>Spring Data module for JPA repositories.</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.springframework.data.domain.Range;
import org.springframework.data.repository.query.SpelQueryContext;
import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor;
import org.springframework.data.repository.query.parser.Part.Type;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -68,6 +68,7 @@ class StringQuery implements DeclaredQuery {

this.bindings = new ArrayList<>();
this.containsPageableInSpel = query.contains("#pageable");

Metadata queryMeta = new Metadata();
this.query = ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query,
this.bindings, queryMeta);
Expand Down Expand Up @@ -178,11 +179,11 @@ public boolean usesPaging() {
*
* @author Thomas Darimont
*/
public enum ParameterBindingParser {
enum ParameterBindingParser {

INSTANCE;

static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__";
private static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__";
public static final String POSITIONAL_OR_INDEXED_PARAMETER = "\\?(\\d*+(?![#\\w]))";
// .....................................................................^ not followed by a hash or a letter.
// .................................................................^ zero or more digits.
Expand All @@ -191,10 +192,9 @@ public enum ParameterBindingParser {
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.";
public static final int INDEXED_PARAMETER_GROUP = 4;
public static final int NAMED_PARAMETER_GROUP = 6;
public static final int COMPARISION_TYPE_GROUP = 1;
public static final int EXPRESSION_GROUP = 9;
private static final int INDEXED_PARAMETER_GROUP = 4;
private static final int NAMED_PARAMETER_GROUP = 6;
private static final int COMPARISION_TYPE_GROUP = 1;

static {

Expand All @@ -218,8 +218,7 @@ public enum ParameterBindingParser {

// named parameter and the parameter name
builder.append("%?(" + QueryUtils.COLON_NO_DOUBLE_COLON + QueryUtils.IDENTIFIER_GROUP + ")%?");
builder.append("|"); // or
builder.append("%?((:|\\?)#\\{([^}]+)\\})%?"); // expression parameter and expression

builder.append(")");
builder.append("\\)?"); // optional braces around parameters

Expand All @@ -230,11 +229,8 @@ public enum ParameterBindingParser {
* Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings. Returns
* the cleaned up query.
*/
String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query, List<ParameterBinding> bindings,
Metadata queryMeta) {

String result = query;
Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(query);
private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query,
List<ParameterBinding> bindings, Metadata queryMeta) {

int greatestParameterIndex = tryFindGreatestParameterIndexIn(query);

Expand All @@ -248,18 +244,18 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que
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;
SpelExtractor spelExtractor = createSpelExtractor(query, parametersShouldBeAccessedByIndex,
greatestParameterIndex);

QuotationMap quotationMap = new QuotationMap(query);
String resultingQuery = spelExtractor.getQueryString();
Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQuery);

int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;

boolean usesJpaStyleParameters = false;
while (matcher.find()) {

if (quotationMap.isQuoted(matcher.start())) {
if (spelExtractor.isQuoted(matcher.start())) {
continue;
}

Expand All @@ -268,33 +264,16 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que
Integer parameterIndex = getParameterIndex(parameterIndexString);

String typeSource = matcher.group(COMPARISION_TYPE_GROUP);
String expression = null;
String expression = spelExtractor.getParameter(parameterName == null ? parameterIndexString : parameterName);
String replacement = null;

if (parameterName == null && parameterIndex == null) {

expressionParameterIndex++;

if ("".equals(parameterIndexString)) {

parameterIndex = expressionParameterIndex;
queryMeta.usesJdbcStyleParameters = true;
} else {

usesJpaStyleParameters = true;
Assert.isTrue(parameterIndexString != null || parameterName != null, "We need either a name or an index.");

if (parametersShouldBeAccessedByIndex) {

parameterIndex = expressionParameterIndex;
replacement = "?" + parameterIndex;
} else {

parameterName = EXPRESSION_PARAMETER_PREFIX + expressionParameterIndex;
replacement = ":" + parameterName;
}
}
expressionParameterIndex++;
if ("".equals(parameterIndexString)) {

expression = matcher.group(EXPRESSION_GROUP);
queryMeta.usesJdbcStyleParameters = true;
parameterIndex = expressionParameterIndex;
} else {
usesJpaStyleParameters = true;
}
Expand All @@ -303,13 +282,12 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que
throw new IllegalArgumentException("Mixing of ? parameters and other forms like ?1 is not supported");
}

String replacementTarget = matcher.group(2);
switch (ParameterBindingType.of(typeSource)) {

case LIKE:

Type likeType = LikeParameterBinding.getLikeTypeFrom(replacementTarget);
replacement = replacement != null ? replacement : matcher.group(3);
Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
replacement = matcher.group(3);

if (parameterIndex != null) {
checkAndRegister(new LikeParameterBinding(parameterIndex, likeType, expression), bindings);
Expand All @@ -336,28 +314,38 @@ String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String que

bindings.add(parameterIndex != null ? new ParameterBinding(null, parameterIndex, expression)
: new ParameterBinding(parameterName, null, expression));

}

if (replacement != null) {
result = replaceFirst(result, replacementTarget, replacement);
resultingQuery = replaceFirst(resultingQuery, matcher.group(2), replacement);
}

}

return result;
return resultingQuery;
}

@Nullable
private Integer getParameterIndex(@Nullable String parameterIndexString) {
private SpelExtractor createSpelExtractor(String queryWithSpel, boolean parametersShouldBeAccessedByIndex,
int greatestParameterIndex) {

if (parameterIndexString == null || parameterIndexString.isEmpty()) {
return null;
}
return Integer.valueOf(parameterIndexString);
/*
* 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;

BiFunction<Integer, String, String> indexToParameterName = parametersShouldBeAccessedByIndex
? (index, expression) -> String.valueOf(index + expressionParameterIndex + 1)
: (index, expression) -> EXPRESSION_PARAMETER_PREFIX + (index + 1);

String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":";

BiFunction<String, String, String> parameterNameToReplacement = (prefix, name) -> fixedPrefix + name;

return SpelQueryContext.of(indexToParameterName, parameterNameToReplacement).parse(queryWithSpel);
}

private static String replaceFirst(String text, String substring, String replacement) {
private String replaceFirst(String text, String substring, String replacement) {

int index = text.indexOf(substring);
if (index < 0) {
Expand All @@ -367,6 +355,15 @@ private static String replaceFirst(String text, String substring, String replace
return text.substring(0, index) + replacement + text.substring(index + substring.length());
}

@Nullable
private Integer getParameterIndex(@Nullable String parameterIndexString) {

if (parameterIndexString == null || parameterIndexString.isEmpty()) {
return null;
}
return Integer.valueOf(parameterIndexString);
}

private int tryFindGreatestParameterIndexIn(String query) {

Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query);
Expand All @@ -386,11 +383,9 @@ private int tryFindGreatestParameterIndexIn(String query) {

private static void checkAndRegister(ParameterBinding binding, List<ParameterBinding> bindings) {

for (ParameterBinding existing : bindings) {
if (existing.hasName(binding.getName()) || existing.hasPosition(binding.getPosition())) {
Assert.isTrue(existing.equals(binding), String.format(MESSAGE, existing, binding));
}
}
bindings.stream() //
.filter(it -> it.hasName(binding.getName()) || it.hasPosition(binding.getPosition())) //
.forEach(it -> Assert.isTrue(it.equals(binding), String.format(MESSAGE, it, binding)));

if (!bindings.contains(binding)) {
bindings.add(binding);
Expand Down Expand Up @@ -835,62 +830,6 @@ private static Type getLikeTypeFrom(String expression) {
}
}

/**
* Value object to analyze a String to determine the parts of the String that are quoted and offers an API to query
* that information.
*
* @author Jens Schauder
* @since 3.0.3
*/
static class QuotationMap {

private static final Set<Character> QUOTING_CHARACTERS = new HashSet<>(Arrays.asList('"', '\''));

private List<Range<Integer>> quotedRanges = new ArrayList<>();

QuotationMap(@Nullable String query) {

if (query == null) {
return;
}

Character inQuotation = null;
int start = 0;

for (int i = 0; i < query.length(); i++) {

char currentChar = query.charAt(i);

if (QUOTING_CHARACTERS.contains(currentChar)) {

if (inQuotation == null) {

inQuotation = currentChar;
start = i;

} else if (currentChar == inQuotation) {

inQuotation = null;
quotedRanges.add(Range.of(Range.Bound.inclusive(start), Range.Bound.inclusive(i)));
}
}
}

if (inQuotation != null) {
throw new IllegalArgumentException(
String.format("The string <%s> starts a quoted range at %d, but never ends it.", query, start));
}
}

/**
* @param index to check if it is part of a quoted range.
* @return whether the query contains a quoted range at {@literal index}.
*/
public boolean isQuoted(int index) {
return quotedRanges.stream().anyMatch(r -> r.contains(index));
}
}

static class Metadata {
private boolean usesJdbcStyleParameters = false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,8 @@
*/
package org.springframework.data.jpa.repository.query;

import java.util.ArrayList;
import java.util.List;

import org.assertj.core.api.SoftAssertions;
import org.junit.Test;
import org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding;
import org.springframework.data.jpa.repository.query.StringQuery.ParameterBindingParser;

/**
Expand Down Expand Up @@ -69,10 +65,9 @@ public void idenficationOfParameters() {

public void checkHasParameter(SoftAssertions softly, String query, boolean containsParameter, String label) {

List<ParameterBinding> bindings = new ArrayList<>();
ParameterBindingParser.INSTANCE.parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, bindings,
new StringQuery.Metadata());
softly.assertThat(bindings.size()) //
StringQuery stringQuery = new StringQuery(query);

softly.assertThat(stringQuery.getParameterBindings().size()) //
.describedAs(String.format("<%s> (%s)", query, label)) //
.isEqualTo(containsParameter ? 1 : 0);
}
Expand Down
Loading