org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of spring-data-couchbase Show documentation
Show all versions of spring-data-couchbase Show documentation
Spring Data integration for Couchbase
/*
* Copyright 2012-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.couchbase.repository.query;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.document.json.JsonValue;
import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.query.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.couchbase.core.CouchbaseOperations;
import org.springframework.data.repository.query.EvaluationContextProvider;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
/**
* A {@link RepositoryQuery} for Couchbase, based on N1QL and a String statement.
*
* The statement can contain positional placeholders (eg. name = $1
) that will map to the
* method's parameters, in the same order.
*
* The statement can also contain SpEL expressions enclosed in #{
and }
.
*
* There are couchbase-provided variables included for the {@link #SPEL_BUCKET bucket namespace},
* the {@link #SPEL_ENTITY ID and CAS fields} necessary for entity reconstruction
* or a shortcut that covers {@link #SPEL_SELECT_FROM_CLAUSE SELECT AND FROM clauses},
* along with a variable for {@link #SPEL_FILTER WHERE clause filtering} of the correct entity.
*
* @author Simon Baslé
*/
public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery {
private static final Logger LOGGER = LoggerFactory.getLogger(StringN1qlBasedQuery.class);
public static final String SPEL_PREFIX = "n1ql";
/**
* Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query}
* annotation's inline statement. This will be replaced by the correct SELECT x FROM y
clause needed
* for entity mapping. Eg. "#{{@value StringN1qlBasedQuery#SPEL_SELECT_FROM_CLAUSE}} WHERE test = true"
.
* Note this only makes sense once, as the beginning of the statement.
*/
public static final String SPEL_SELECT_FROM_CLAUSE = "#" + SPEL_PREFIX + ".selectEntity";
/**
* Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query}
* annotation's inline statement. This will be replaced by the (escaped) bucket name corresponding to the repository's
* entity. Eg. "SELECT * FROM #{{@value StringN1qlBasedQuery#SPEL_BUCKET}} LIMIT 3"
.
*/
public static final String SPEL_BUCKET = "#" + SPEL_PREFIX + ".bucket";
/**
* Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query}
* annotation's inline statement. This will be replaced by the fields allowing to construct the repository's entity
* (SELECT clause). Eg. "SELECT #{{@value StringN1qlBasedQuery#SPEL_ENTITY}} FROM test"
.
*/
public static final String SPEL_ENTITY = "#" + SPEL_PREFIX + ".fields";
/**
* Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.query.Query @Query}
* annotation's inline statement WHERE clause. This will be replaced by the expression allowing to only select
* documents matching the entity's class. Eg. "SELECT * FROM test WHERE test = true AND #{{@value StringN1qlBasedQuery#SPEL_FILTER}}"
.
*/
public static final String SPEL_FILTER = "#" + SPEL_PREFIX + ".filter";
/** regexp that detect $named placeholder (starts with a letter, then alphanum chars) */
private static final Pattern NAMED_PLACEHOLDER_PATTERN = Pattern.compile("\\W(\\$\\p{Alpha}\\p{Alnum}*)\\b");
/** regexp that detect positional placeholder ($ followed by digits only) */
private static final Pattern POSITIONAL_PLACEHOLDER_PATTERN = Pattern.compile("\\W(\\$\\p{Digit}+)\\b");
/** regexp that detects " and ' quote boundaries, ignoring escaped quotes */
private static final Pattern QUOTE_DETECTION_PATTERN = Pattern.compile("[\"'](?:[^\"'\\\\]*(?:\\\\.)?)*[\"']");
/** enumeration of all the combinations of placeholder types that could be found in a N1QL statement */
private enum PlaceholderType {
NAMED, POSITIONAL, NONE
}
private final String originalStatement;
private final PlaceholderType placeHolderType;
private final SpelExpressionParser parser;
private final EvaluationContextProvider evaluationContextProvider;
private final N1qlSpelValues countContext;
private final N1qlSpelValues statementContext;
protected String getTypeField() {
return getCouchbaseOperations().getConverter().getTypeKey();
}
protected Class getTypeValue() {
return getQueryMethod().getEntityInformation().getJavaType();
}
public StringN1qlBasedQuery(String statement, CouchbaseQueryMethod queryMethod, CouchbaseOperations couchbaseOperations,
SpelExpressionParser spelParser, final EvaluationContextProvider evaluationContextProvider) {
super(queryMethod, couchbaseOperations);
this.originalStatement = statement;
this.placeHolderType = checkPlaceholders(statement);
this.parser = spelParser;
this.evaluationContextProvider = evaluationContextProvider;
this.statementContext = createN1qlSpelValues(getCouchbaseOperations().getCouchbaseBucket().name(), getTypeField(), getTypeValue(), false);
this.countContext = createN1qlSpelValues(getCouchbaseOperations().getCouchbaseBucket().name(), getTypeField(), getTypeValue(), true);
}
private PlaceholderType checkPlaceholders(String statement) {
Matcher quoteMatcher = QUOTE_DETECTION_PATTERN.matcher(statement);
Matcher positionMatcher = POSITIONAL_PLACEHOLDER_PATTERN.matcher(statement);
Matcher namedMatcher = NAMED_PLACEHOLDER_PATTERN.matcher(statement);
List quotes = new ArrayList();
while(quoteMatcher.find()) {
quotes.add(new int[] { quoteMatcher.start(), quoteMatcher.end() });
}
int posCount = 0;
int namedCount = 0;
while(positionMatcher.find()) {
String placeholder = positionMatcher.group(1);
//check not in quoted
if (checkNotQuoted(placeholder, positionMatcher.start(), positionMatcher.end(), quotes)) {
LOGGER.trace("{}: Found positional placeholder {}", getQueryMethod().getName(), placeholder);
posCount++;
}
}
while(namedMatcher.find()) {
String placeholder = namedMatcher.group(1);
//check not in quoted
if (checkNotQuoted(placeholder, namedMatcher.start(), namedMatcher.end(), quotes)) {
LOGGER.trace("{}: Found named placeholder {}", getQueryMethod().getName(), placeholder);
namedCount++;
}
}
if (posCount > 0 && namedCount > 0) {
throw new IllegalArgumentException("Using both named (" + namedCount + ") and positional (" + posCount +
") placeholders is not supported, please choose one over the other in " + queryMethod.getName());
} else if (posCount > 0) {
return PlaceholderType.POSITIONAL;
} else if (namedCount > 0) {
return PlaceholderType.NAMED;
} else {
return PlaceholderType.NONE;
}
}
private boolean checkNotQuoted(String item, int start, int end, List quotes) {
for (int[] quote : quotes) {
if (quote[0] <= start && quote[1] >= end) {
LOGGER.trace("{}: potential placeholder {} is inside quotes, ignored", queryMethod.getName(), item);
return false;
}
}
return true;
}
public static N1qlSpelValues createN1qlSpelValues(String bucketName, String typeField, Class typeValue, boolean isCount) {
String b = "`" + bucketName + "`";
String entity = "META(" + b + ").id AS " + CouchbaseOperations.SELECT_ID +
", META(" + b + ").cas AS " + CouchbaseOperations.SELECT_CAS;
String count = "COUNT(*) AS " + CountFragment.COUNT_ALIAS;
String selectEntity;
if (isCount) {
selectEntity = "SELECT " + count + " FROM " + b;
} else {
selectEntity = "SELECT " + entity + ", " + b + ".* FROM " + b;
}
String typeSelection = "`" + typeField + "` = \"" + typeValue.getName() + "\"";
return new N1qlSpelValues(selectEntity, entity, b, typeSelection);
}
/**
* Parse the statement to detect SPEL blocks (delimited by #{
and }
)
* and replace said expression blocks with their corresponding values.
*
* @param statement the full statement into which SpEL expressions should be parsed and replaced.
* @param runtimeParameters the parameters passed into the method at runtime.
* @return the statement with the SpEL interpreted and replaced by its values.
*/
protected String parseSpel(String statement, boolean isCount, Object[] runtimeParameters) {
EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(getQueryMethod().getParameters(), runtimeParameters);
N1qlSpelValues n1qlSpelValues = this.statementContext;
if (isCount) {
n1qlSpelValues = this.countContext;
}
return doParse(statement, parser, evaluationContext, n1qlSpelValues);
}
//this static method can be used to test the parsing behavior for Couchbase specific spel variables
//in isolation from the rest of the spel parser initialization chain.
protected static String doParse(String statement, SpelExpressionParser parser, EvaluationContext evaluationContext, N1qlSpelValues n1qlSpelValues) {
Expression parsedExpression = parser.parseExpression(statement, new TemplateParserContext());
evaluationContext.setVariable(SPEL_PREFIX, n1qlSpelValues);
return parsedExpression.getValue(evaluationContext, String.class);
}
@Override
protected JsonValue getPlaceholderValues(ParameterAccessor accessor) {
switch (this.placeHolderType) {
case NAMED:
return getNamedPlaceholderValues(accessor);
case POSITIONAL:
return getPositionalPlaceholderValues(accessor);
case NONE:
default:
return JsonArray.empty();
}
}
private JsonValue getPositionalPlaceholderValues(ParameterAccessor accessor) {
JsonArray posValues = JsonArray.create();
for (Parameter parameter : getQueryMethod().getParameters().getBindableParameters()) {
posValues.add(accessor.getBindableValue(parameter.getIndex()));
}
return posValues;
}
private JsonObject getNamedPlaceholderValues(ParameterAccessor accessor) {
JsonObject namedValues = JsonObject.create();
for (Parameter parameter : getQueryMethod().getParameters().getBindableParameters()) {
String placeholder = parameter.getPlaceholder();
Object value = accessor.getBindableValue(parameter.getIndex());
if (placeholder != null && placeholder.charAt(0) == ':') {
placeholder = placeholder.replaceFirst(":", "");
namedValues.put(placeholder, value);
} else {
namedValues.put(parameter.getName(), value);
}
}
return namedValues;
}
@Override
public Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters) {
String parsedStatement = parseSpel(this.originalStatement, false, runtimeParameters);
return N1qlQuery.simple(parsedStatement).statement();
}
@Override
protected Statement getCount(ParameterAccessor accessor, Object[] runtimeParameters) {
String parsedCountStatement = parseSpel(this.originalStatement, true, runtimeParameters);
return N1qlQuery.simple(parsedCountStatement).statement();
}
@Override
protected boolean useGeneratedCountQuery() {
return this.originalStatement.contains(SPEL_SELECT_FROM_CLAUSE);
}
/**
* This class is exposed to SpEL parsing through the variable #{@value StringN1qlBasedQuery#SPEL_PREFIX}
.
* Use the attributes in your SpEL expressions: {@link #selectEntity}, {@link #fields}, {@link #bucket} and {@link #filter}.
*/
public static final class N1qlSpelValues {
/**
* #{{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
* selectEntity
will be replaced by the SELECT-FROM clause corresponding to the entity. Use once at the beginning.
*/
public final String selectEntity;
/**
* #{{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
* fields
will be replaced by the list of N1QL fields allowing to reconstruct the entity.
*/
public final String fields;
/**
* #{{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
* bucket
will be replaced by (escaped) bucket name in which the entity is stored.
*/
public final String bucket;
/**
* #{{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
* filter
will be replaced by an expression allowing to select only entries matching the entity in a WHERE clause.
*/
public final String filter;
public N1qlSpelValues(String selectClause, String entityFields, String bucket, String filter) {
this.selectEntity = selectClause;
this.fields = entityFields;
this.bucket = bucket;
this.filter = filter;
}
}
}