/*
* Copyright 2016-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.cassandra.repository.query;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.data.cassandra.repository.query.ExpressionEvaluatingParameterBinder.BindingContext;
import org.springframework.data.cassandra.repository.query.ExpressionEvaluatingParameterBinder.ParameterBinding;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.datastax.driver.core.CodecRegistry;
import com.datastax.driver.core.SimpleStatement;
/**
* String-based Query abstracting a CQL query with parameter bindings.
*
* @author Mark Paluch
* @since 2.0
*/
class StringBasedQuery {
private final String query;
private final ExpressionEvaluatingParameterBinder parameterBinder;
private final List<ParameterBinding> queryParameterBindings = new ArrayList<>();
/**
* Create a new {@link StringBasedQuery} given {@code query}, {@link ExpressionEvaluatingParameterBinder} and
* {@link CodecRegistry}.
*
* @param query must not be empty.
* @param parameterBinder must not be {@literal null}.
*/
public StringBasedQuery(String query, ExpressionEvaluatingParameterBinder parameterBinder) {
Assert.hasText(query, "Query must not be empty");
Assert.notNull(parameterBinder, "ExpressionEvaluatingParameterBinder must not be null");
this.parameterBinder = parameterBinder;
this.query = ParameterBindingParser.INSTANCE.parseAndCollectParameterBindingsFromQueryIntoBindings(query,
this.queryParameterBindings);
}
/* (non-Javadoc) */
protected ExpressionEvaluatingParameterBinder getParameterBinder() {
return this.parameterBinder;
}
/* (non-Javadoc) */
protected String getQuery() {
return this.query;
}
/**
* Bind the query to actual parameters using {@link CassandraParameterAccessor},
*
* @param parameterAccessor must not be {@literal null}.
* @param queryMethod must not be {@literal null}.
* @return the bound String query containing formatted parameters.
*/
public SimpleStatement bindQuery(CassandraParameterAccessor parameterAccessor, CassandraQueryMethod queryMethod) {
Assert.notNull(parameterAccessor, "CassandraParameterAccessor must not be null");
Assert.notNull(queryMethod, "CassandraQueryMethod must not be null");
List<Object> arguments = getParameterBinder().bind(parameterAccessor,
new BindingContext(queryMethod, this.queryParameterBindings));
return ParameterBinder.INSTANCE.bind(getQuery(), arguments);
}
/**
* A parser that extracts the parameter bindings from a given query string.
*
* @author Mark Paluch
*/
enum ParameterBinder {
INSTANCE;
private static final String ARGUMENT_PLACEHOLDER = "?_param_?";
private static final Pattern ARGUMENT_PLACEHOLDER_PATTERN = Pattern.compile(Pattern.quote(ARGUMENT_PLACEHOLDER));
public SimpleStatement bind(String input, List<Object> parameters) {
if (parameters.isEmpty()) {
return new SimpleStatement(input);
}
StringBuilder result = new StringBuilder();
int startIndex = 0;
int currentPosition = 0;
int parameterIndex = 0;
Matcher matcher = ARGUMENT_PLACEHOLDER_PATTERN.matcher(input);
while (currentPosition < input.length()) {
if (!matcher.find()) {
break;
}
int exprStart = matcher.start();
result.append(input.subSequence(startIndex, exprStart)).append("?");
parameterIndex++;
currentPosition = matcher.end();
startIndex = currentPosition;
}
String bindableStatement = result.append(input.subSequence(currentPosition, input.length())).toString();
return new SimpleStatement(bindableStatement, parameters.subList(0, parameterIndex).toArray());
}
}
/**
* A parser that extracts the parameter bindings from a given query string.
*
* @author Mark Paluch
*/
enum ParameterBindingParser {
INSTANCE;
private static final char CURRLY_BRACE_OPEN = '{';
private static final char CURRLY_BRACE_CLOSE = '}';
private static final Pattern INDEX_PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
private static final Pattern NAMED_PARAMETER_BINDING_PATTERN = Pattern.compile("\\:(\\w+)");
private static final Pattern INDEX_BASED_EXPRESSION_PATTERN = Pattern.compile("\\?\\#\\{");
private static final Pattern NAME_BASED_EXPRESSION_PATTERN = Pattern.compile("\\:\\#\\{");
private static final String ARGUMENT_PLACEHOLDER = "?_param_?";
/**
* Returns a list of {@link ParameterBinding}s found in the given {@code input}.
*
* @param input can be {@literal null} or empty.
* @param bindings must not be {@literal null}.
* @return a list of {@link ParameterBinding}s found in the given {@code input}.
*/
public String parseAndCollectParameterBindingsFromQueryIntoBindings(String input, List<ParameterBinding> bindings) {
if (!StringUtils.hasText(input)) {
return input;
}
Assert.notNull(bindings, "Parameter bindings must not be null");
return transformQueryAndCollectExpressionParametersIntoBindings(input, bindings);
}
private static String transformQueryAndCollectExpressionParametersIntoBindings(String input,
List<ParameterBinding> bindings) {
StringBuilder result = new StringBuilder();
int startIndex = 0;
int currentPosition = 0;
while (currentPosition < input.length()) {
Matcher matcher = findNextBindingOrExpression(input, currentPosition);
// no expression parameter found
if (matcher == null) {
break;
}
int exprStart = matcher.start();
currentPosition = exprStart;
if (matcher.pattern() == NAME_BASED_EXPRESSION_PATTERN || matcher.pattern() == INDEX_BASED_EXPRESSION_PATTERN) {
// eat parameter expression
int curlyBraceOpenCount = 1;
currentPosition += 3;
while (curlyBraceOpenCount > 0 && currentPosition < input.length()) {
switch (input.charAt(currentPosition++)) {
case CURRLY_BRACE_OPEN:
curlyBraceOpenCount++;
break;
case CURRLY_BRACE_CLOSE:
curlyBraceOpenCount--;
break;
default:
}
}
result.append(input.subSequence(startIndex, exprStart));
} else {
result.append(input.subSequence(startIndex, exprStart));
}
result.append(ARGUMENT_PLACEHOLDER);
if (matcher.pattern() == NAME_BASED_EXPRESSION_PATTERN || matcher.pattern() == INDEX_BASED_EXPRESSION_PATTERN) {
bindings.add(ExpressionEvaluatingParameterBinder.ParameterBinding
.expression(input.substring(exprStart + 3, currentPosition - 1), true));
} else {
if (matcher.pattern() == INDEX_PARAMETER_BINDING_PATTERN) {
bindings
.add(ExpressionEvaluatingParameterBinder.ParameterBinding.indexed(Integer.parseInt(matcher.group(1))));
} else {
bindings.add(ExpressionEvaluatingParameterBinder.ParameterBinding.named(matcher.group(1)));
}
currentPosition = matcher.end();
}
startIndex = currentPosition;
}
return result.append(input.subSequence(currentPosition, input.length())).toString();
}
private static Matcher findNextBindingOrExpression(String input, int position) {
List<Matcher> matchers = new ArrayList<>();
matchers.add(INDEX_PARAMETER_BINDING_PATTERN.matcher(input));
matchers.add(NAMED_PARAMETER_BINDING_PATTERN.matcher(input));
matchers.add(INDEX_BASED_EXPRESSION_PATTERN.matcher(input));
matchers.add(NAME_BASED_EXPRESSION_PATTERN.matcher(input));
TreeMap<Integer, Matcher> matcherMap = new TreeMap<>();
for (Matcher matcher : matchers) {
if (matcher.find(position)) {
matcherMap.put(matcher.start(), matcher);
}
}
return (matcherMap.isEmpty() ? null : matcherMap.values().iterator().next());
}
}
}