diff --git a/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java b/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java index 4252e552..e325697a 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java @@ -139,6 +139,14 @@ public class SimpleCouchbaseRepositoryTests { } } + @Test + public void shouldFindFromUsernameInlineWithSpelParsing() { + User user = repository.findByUsernameWithSpelAndPlaceholder(); + assertNotNull(user); + assertEquals("testuser-4", user.getKey()); + assertEquals("uname-4", user.getUsername()); + } + @Test public void shouldFindFromDeriveQueryWithRegexpAndIn() { User user = repository.findByUsernameRegexAndUsernameIn("uname-[123]", Arrays.asList("uname-2", "uname-4")); diff --git a/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java index c4a11623..aab82796 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java @@ -34,12 +34,15 @@ public interface UserRepository extends CouchbaseRepository { @View(designDocument = "user", viewName = "all") Iterable customViewQuery(ViewQuery query); - @Query("$SELECT_ENTITY$ WHERE username = $1") + @Query("${#n1ql.selectEntity} WHERE username = $1") User findByUsername(String username); - @Query("SELECT * FROM $BUCKET$ WHERE username = $1") + @Query("SELECT * FROM ${#n1ql.bucket} WHERE username = $1") User findByUsernameBadSelect(String username); + @Query("${#n1ql.selectEntity} WHERE username LIKE '%-${3 + 1}'") + User findByUsernameWithSpelAndPlaceholder(); + @Query User findByUsernameRegexAndUsernameIn(String regex, List sample); diff --git a/src/main/asciidoc/repository.adoc b/src/main/asciidoc/repository.adoc index f7ce885d..bf84b817 100644 --- a/src/main/asciidoc/repository.adoc +++ b/src/main/asciidoc/repository.adoc @@ -119,7 +119,7 @@ Here is an example: ---- public interface UserRepository extends CrudRepository { - @Query("$SELECT_ENTITY$ WHERE role = 'admin' AND $FILTER_TYPE$") + @Query("${#n1ql.selectEntity} WHERE role = 'admin' AND ${#n1ql.filter}") List findAllAdmins(); List findByFirstname(String fname); @@ -129,12 +129,18 @@ public interface UserRepository extends CrudRepository { Here we see two N1QL-backed ways of querying. -The first one uses the `Query` annotation to provide a N1QL statement inline. Notice the special placeholders: +The first method uses the `Query` annotation to provide a N1QL statement inline. SpEL (Spring Expression Language) is supported by surrounding SpEL expressions between `${` and `}`. You can also include N1QL positional placeholders (eg. `$1`, `$2`). +A few N1QL-specific values are provided through SpEL: - - `$SELECT_ENTITY` allows to easily make sure the statement will select all the fields necessary to build the full entity (including document ID and CAS value). - - `$FILTER_TYPE$` in the WHERE clause adds a criteria matching the entity type with the field that Spring Data uses to store type information. + - `#n1ql.selectEntity` allows to easily make sure the statement will select all the fields necessary to build the full entity (including document ID and CAS value). + - `#n1ql.filter` in the WHERE clause adds a criteria matching the entity type with the field that Spring Data uses to store type information. + - `#n1ql.bucket` will be replaced by the name of the bucket the entity is stored in, escaped in backticks. + - `#n1ql.fields` will be replaced by the list of fields (eg. for a SELECT clause) necessary to reconstruct the entity. -The second one use Spring-Data's query derivation mechanism to build a N1QL query from the method name and parameters. This will produce a query looking like this: `SELECT ... FROM ... WHERE firstName = "valueOfFnameAtRuntime"`. You can combine these criteria, even do a count with a name like `countByFirstname` or a limit with a name like `findFirst3ByLastname`... +Another example: "`${#n1ql.selectEntity} WHERE ${#n1ql.filter} AND test = $1`", which is equivalent to +`SELECT ${#n1ql.fields} FROM ${#n1ql.bucket} WHERE ${#n1ql.filter} AND test = $1`". + +The second method uses Spring-Data's query derivation mechanism to build a N1QL query from the method name and parameters. This will produce a query looking like this: `SELECT ... FROM ... WHERE firstName = "valueOfFnameAtRuntime"`. You can combine these criteria, even do a count with a name like `countByFirstname` or a limit with a name like `findFirst3ByLastname`... NOTE: Actually the generated N1QL query will also contain an additional N1QL criteria in order to only select documents that match the repository's entity class. diff --git a/src/main/java/org/springframework/data/couchbase/core/view/Query.java b/src/main/java/org/springframework/data/couchbase/core/view/Query.java index f4a022d3..6e4c6c95 100644 --- a/src/main/java/org/springframework/data/couchbase/core/view/Query.java +++ b/src/main/java/org/springframework/data/couchbase/core/view/Query.java @@ -34,24 +34,25 @@ import org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery; *

* In this case, one can use a placeholder notation of {@code ?0}, {@code ?1} and so on. *

- * Also, the following placeholders can be used to be replaced by the underlying {@link CouchbaseTemplate} + * Also, SpEL in the form ${spelExpression} is supported, including the + * following N1QL variables that will be replaced by the underlying {@link CouchbaseTemplate} * associated information: *

    *
  • - * {@value StringN1qlBasedQuery#PLACEHOLDER_SELECT_FROM} - * (see {@link StringN1qlBasedQuery#PLACEHOLDER_SELECT_FROM}) + * {@value StringN1qlBasedQuery#SPEL_SELECT_FROM_CLAUSE} + * (see {@link StringN1qlBasedQuery#SPEL_SELECT_FROM_CLAUSE}) *
  • *
  • - * {@value StringN1qlBasedQuery#PLACEHOLDER_BUCKET} - * (see {@link StringN1qlBasedQuery#PLACEHOLDER_BUCKET}) + * {@value StringN1qlBasedQuery#SPEL_BUCKET} + * (see {@link StringN1qlBasedQuery#SPEL_BUCKET}) *
  • *
  • - * {@value StringN1qlBasedQuery#PLACEHOLDER_ENTITY} - * (see {@link StringN1qlBasedQuery#PLACEHOLDER_ENTITY}) + * {@value StringN1qlBasedQuery#SPEL_ENTITY} + * (see {@link StringN1qlBasedQuery#SPEL_ENTITY}) *
  • *
  • - * {@value StringN1qlBasedQuery#PLACEHOLDER_FILTER_TYPE} - * (see {@link StringN1qlBasedQuery#PLACEHOLDER_FILTER_TYPE}) + * {@value StringN1qlBasedQuery#SPEL_FILTER} + * (see {@link StringN1qlBasedQuery#SPEL_FILTER}) *
  • *
* diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java index c40966f1..500f3453 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java @@ -20,7 +20,6 @@ import java.util.List; import com.couchbase.client.java.document.json.JsonArray; import com.couchbase.client.java.query.N1qlQuery; -import com.couchbase.client.java.query.N1qlQueryResult; import com.couchbase.client.java.query.N1qlParams; import com.couchbase.client.java.query.Statement; import com.couchbase.client.java.query.consistency.ScanConsistency; @@ -28,7 +27,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.CouchbaseOperations; -import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.SliceImpl; @@ -55,16 +53,16 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { this.couchbaseOperations = couchbaseOperations; } - protected abstract Statement getCount(ParameterAccessor accessor); + protected abstract Statement getCount(ParameterAccessor accessor, Object[] runtimeParameters); - protected abstract Statement getStatement(ParameterAccessor accessor); + protected abstract Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters); protected abstract JsonArray getPlaceholderValues(ParameterAccessor accessor); @Override public Object execute(Object[] parameters) { - ParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); - Statement statement = getStatement(accessor); + ParametersParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); + Statement statement = getStatement(accessor, parameters); JsonArray queryPlaceholderValues = getPlaceholderValues(accessor); //prepare the final query @@ -73,7 +71,7 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { //prepare a count query //TODO only do that when necessary (isPageQuery or isSliceQuery) - Statement countStatement = getCount(accessor); + Statement countStatement = getCount(accessor, parameters); N1qlQuery countQuery = buildQuery(countStatement, queryPlaceholderValues, getCouchbaseOperations().getDefaultConsistency().n1qlConsistency()); diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java index 68bc2dab..51a7cc98 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java @@ -51,7 +51,7 @@ public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery { } @Override - protected Statement getCount(ParameterAccessor accessor) { + protected Statement getCount(ParameterAccessor accessor, Object[] runtimeParameters) { Expression bucket = i(getCouchbaseOperations().getCouchbaseBucket().name()); WherePath countFrom = select(count("*").as(CountFragment.COUNT_ALIAS)).from(bucket); @@ -60,7 +60,7 @@ public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery { return queryCreator.createQuery(); } @Override - protected Statement getStatement(ParameterAccessor accessor) { + protected Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters) { String bucketName = getCouchbaseOperations().getCouchbaseBucket().name(); Expression bucket = N1qlUtils.escapedBucket(bucketName); diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java index a9b4b3a6..c0f83c89 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java @@ -19,10 +19,16 @@ package org.springframework.data.couchbase.repository.query; import com.couchbase.client.java.document.json.JsonArray; 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.ParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; /** * A {@link RepositoryQuery} for Couchbase, based on N1QL and a String statement. @@ -30,51 +36,77 @@ import org.springframework.data.repository.query.RepositoryQuery; * The statement can contain positional placeholders (eg. name = $1) that will map to the * method's parameters, in the same order. *

- * The statement can contain placeholders for the {@link #PLACEHOLDER_BUCKET bucket namespace}, - * the {@link #PLACEHOLDER_ENTITY ID and CAS fields} necessary for entity reconstruction or - * a shortcut that covers {@link #PLACEHOLDER_SELECT_FROM SELECT AND FROM clauses}. + * The statement can also contain SpEL expressions enclosed in ${}. + *

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 { - /** - * Use this placeholder in a {@link org.springframework.data.couchbase.core.view.Query @Query} annotation's inline - * statement. This will be replaced by the correct SELECT x FROM y clause needed for entity mapping. - */ - public static final String PLACEHOLDER_SELECT_FROM = "$SELECT_ENTITY$"; + private static final Logger LOGGER = LoggerFactory.getLogger(StringN1qlBasedQuery.class); + + + public static final String SPEL_PREFIX = "n1ql"; /** - * Use this placeholder in a {@link org.springframework.data.couchbase.core.view.Query @Query} annotation's inline - * statement. This will be replaced by the bucket name corresponding to the repository's entity. + * Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.view.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 PLACEHOLDER_BUCKET = "$BUCKET$"; + public static final String SPEL_SELECT_FROM_CLAUSE = "#" + SPEL_PREFIX + ".selectEntity"; /** - * Use this placeholder in a {@link org.springframework.data.couchbase.core.view.Query @Query} annotation's inline - * statement. This will be replaced by the fields allowing to construct the repository's entity (SELECT clause). + * Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.view.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 PLACEHOLDER_ENTITY = "$ENTITY$"; + public static final String SPEL_BUCKET = "#" + SPEL_PREFIX + ".bucket"; /** - * Use this placeholder in a {@link org.springframework.data.couchbase.core.view.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. + * Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.view.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 PLACEHOLDER_FILTER_TYPE = "$FILTER_TYPE$"; + public static final String SPEL_ENTITY = "#" + SPEL_PREFIX + ".fields"; - private final Statement statement; - private final Statement countStatement; + /** + * Use this variable in a SpEL expression in a {@link org.springframework.data.couchbase.core.view.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"; - public StringN1qlBasedQuery(String statement, CouchbaseQueryMethod queryMethod, CouchbaseOperations couchbaseOperations) { - super(queryMethod, couchbaseOperations); - String typeField = getCouchbaseOperations().getConverter().getTypeKey(); - Class typeValue = getQueryMethod().getEntityInformation().getJavaType(); - this.statement = prepare(statement, couchbaseOperations.getCouchbaseBucket().name(), typeField, typeValue, false); - this.countStatement = prepare(statement, couchbaseOperations.getCouchbaseBucket().name(), typeField, typeValue, true); + private final String originalStatement; + private final SpelExpressionParser parser; + private final EvaluationContextProvider evaluationContextProvider; + private final N1qlSpelValues countContext; + private final N1qlSpelValues statementContext; + + protected String getTypeField() { + return getCouchbaseOperations().getConverter().getTypeKey(); } - protected static Statement prepare(String statement, String bucketName, String typeField, Class typeValue, boolean isCount) { + 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.parser = spelParser; + this.evaluationContextProvider = evaluationContextProvider; + + this.statementContext = createN1qlSpelValues(getCouchbaseOperations().getCouchbaseBucket().name(), getTypeField(), getTypeValue(), false); + this.countContext = createN1qlSpelValues(getCouchbaseOperations().getCouchbaseBucket().name(), getTypeField(), getTypeValue(), 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; @@ -87,23 +119,85 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery { } String typeSelection = "`" + typeField + "` = \"" + typeValue.getName() + "\""; - String result = statement; - if (statement.contains(PLACEHOLDER_SELECT_FROM)) { - result = result.replaceFirst("\\$SELECT_ENTITY\\$", selectEntity); - } else { - if (statement.contains(PLACEHOLDER_BUCKET)) { - result = result.replaceAll("\\$BUCKET\\$", b); + return new N1qlSpelValues(selectEntity, entity, b, typeSelection); + } + + /** + * Parse the statement to detect SPEL expressions (delimited by ${ and }) + * and replace said expressions with their corresponding values (see {@link #evaluateSpelExpression(String, boolean, Object[])}). + * + * @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) { + StringBuilder parsed = new StringBuilder(statement); + + int currentIndex = 0; + while(currentIndex > -1 && currentIndex < parsed.length()) { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Evaluating from index {}, {}", currentIndex, parsed.substring(currentIndex)); } - if (statement.contains(PLACEHOLDER_ENTITY)) { - result = result.replaceFirst("\\$ENTITY\\$", entity); + int opening = parsed.indexOf("${", currentIndex); + if (opening == -1) { + break; + } else { + //find the end of the ${ } expression, eat inner braces + int closing = opening + 2; + int curlyBraceOpenCnt = 1; + + while (curlyBraceOpenCnt > 0) { + switch (parsed.charAt(closing++)) { + case '{': + curlyBraceOpenCnt++; + break; + case '}': + curlyBraceOpenCnt--; + break; + default: + } + } + + //we're now at the end of the expression + //evaluate and replace + String expression = parsed.substring(opening + 2, closing - 1); + String replacement = evaluateSpelExpression(expression, isCount, runtimeParameters); + + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Replacing SPEL \"{}\" with value \"{}\"", expression, replacement); + } + + parsed.replace(opening, closing, replacement); + + //move on + currentIndex = opening + replacement.length(); } } + return parsed.toString(); + } - if (statement.contains(PLACEHOLDER_FILTER_TYPE)) { - result = result.replaceFirst("\\$FILTER_TYPE\\$", typeSelection); + /** + * This method is in charge of evaluating a single SPEL expression (as delimited by ${ and }) + * and returning the corresponding computed value. + * + * @param expression the SPEL expression. + * @param runtimeParameters the parameters using during method call. + * @return the corresponding value. + */ + protected String evaluateSpelExpression(String expression, boolean isCount, Object[] runtimeParameters) { + EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext(getQueryMethod().getParameters(), runtimeParameters); + Expression parsedExpression = parser.parseExpression(expression); + N1qlSpelValues n1qlSpelValues = this.statementContext; + if (isCount) { + n1qlSpelValues = this.countContext; } - return N1qlQuery.simple(result).statement(); + return doEvaluate(evaluationContext, parsedExpression, n1qlSpelValues); + } + + protected static String doEvaluate(EvaluationContext context, Expression parsedExpression, N1qlSpelValues spelValues) { + context.setVariable(SPEL_PREFIX, spelValues); + return parsedExpression.getValue(context, String.class); } @Override @@ -116,12 +210,52 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery { } @Override - public Statement getStatement(ParameterAccessor accessor) { - return this.statement; + 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) { - return this.countStatement; + protected Statement getCount(ParameterAccessor accessor, Object[] runtimeParameters) { + String parsedCountStatement = parseSpel(this.originalStatement, true, runtimeParameters); + return N1qlQuery.simple(parsedCountStatement).statement(); + } + + /** + * 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; + } } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java index d4a94a5b..c2cb00cd 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java @@ -44,6 +44,7 @@ import org.springframework.data.repository.core.support.RepositoryFactorySupport import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; /** @@ -53,6 +54,8 @@ import org.springframework.util.Assert; */ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { + private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser(); + /** * Holds the reference to the template. */ @@ -170,13 +173,20 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { @Override protected QueryLookupStrategy getQueryLookupStrategy(QueryLookupStrategy.Key key, EvaluationContextProvider contextProvider) { - return new CouchbaseQueryLookupStrategy(); + return new CouchbaseQueryLookupStrategy(contextProvider); } /** * Strategy to lookup Couchbase queries implementation to be used. */ private class CouchbaseQueryLookupStrategy implements QueryLookupStrategy { + + private final EvaluationContextProvider evaluationContextProvider; + + public CouchbaseQueryLookupStrategy(EvaluationContextProvider evaluationContextProvider) { + this.evaluationContextProvider = evaluationContextProvider; + } + @Override public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, NamedQueries namedQueries) { CouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve(metadata.getRepositoryInterface(), @@ -191,10 +201,12 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { return new ViewBasedCouchbaseQuery(queryMethod, couchbaseOperations); } else if (queryMethod.hasN1qlAnnotation()) { if (queryMethod.hasInlineN1qlQuery()) { - return new StringN1qlBasedQuery(queryMethod.getInlineN1qlQuery(), queryMethod, couchbaseOperations); + return new StringN1qlBasedQuery(queryMethod.getInlineN1qlQuery(), queryMethod, couchbaseOperations, + SPEL_PARSER, evaluationContextProvider); } else if (namedQueries.hasQuery(namedQueryName)) { String namedQuery = namedQueries.getQuery(namedQueryName); - return new StringN1qlBasedQuery(namedQuery, queryMethod, couchbaseOperations); + return new StringN1qlBasedQuery(namedQuery, queryMethod, couchbaseOperations, + SPEL_PARSER, evaluationContextProvider); } //otherwise will do default, queryDerivation } return new PartTreeN1qlBasedQuery(queryMethod, couchbaseOperations); diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java index 1ec8edc4..9ee26522 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1QlBasedQueryTest.java @@ -1,53 +1,120 @@ package org.springframework.data.couchbase.repository.query; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery.*; -import com.couchbase.client.java.query.Statement; +import org.junit.Before; import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; public class StringN1QlBasedQueryTest { + private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser(); + private StringN1qlBasedQuery mockStringUnderscoreClass; + private StringN1qlBasedQuery mockStringAtClass; + + @Before + public void initMock() { + N1qlSpelValues contextStringUnderscoreClass = StringN1qlBasedQuery.createN1qlSpelValues("B", "_class", String.class, false); + N1qlSpelValues contextCountStringUnderscoreClass = StringN1qlBasedQuery.createN1qlSpelValues("B", "_class", String.class, true); + N1qlSpelValues contextStringAtClass = StringN1qlBasedQuery.createN1qlSpelValues("B", "@class", String.class, false); + N1qlSpelValues contextCountStringAtClass = StringN1qlBasedQuery.createN1qlSpelValues("B", "@class", String.class, true); + + mockStringUnderscoreClass = mock(StringN1qlBasedQuery.class); + when(mockStringUnderscoreClass.parseSpel(anyString(), anyBoolean(), any(Object[].class))).thenCallRealMethod(); + when(mockStringUnderscoreClass.evaluateSpelExpression(anyString(), anyBoolean(), any(Object[].class))) + .thenAnswer(mockSpelEvaluation(contextStringUnderscoreClass, contextCountStringUnderscoreClass)); + + mockStringAtClass = mock(StringN1qlBasedQuery.class); + when(mockStringAtClass.parseSpel(anyString(),anyBoolean(), any(Object[].class))).thenCallRealMethod(); + when(mockStringAtClass.evaluateSpelExpression(anyString(), anyBoolean(), any(Object[].class))) + .thenAnswer(mockSpelEvaluation(contextStringAtClass, contextCountStringAtClass)); + } + + private static Answer mockSpelEvaluation(final N1qlSpelValues spelValues, final N1qlSpelValues countSpelValues) { + return new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + EvaluationContext context = new StandardEvaluationContext(); + Expression expression = SPEL_PARSER.parseExpression((String) invocation.getArguments()[0]); + boolean isCount = (Boolean) invocation.getArguments()[1]; + if (isCount) + return doEvaluate(context, expression, countSpelValues); + else + return doEvaluate(context, expression, spelValues); + } + }; + } + + private static String spel(String expression) { + return "${" + expression + "}"; + } + @Test - public void testReplaceFullSelectPlaceholderOnce() throws Exception { - String statement = PLACEHOLDER_SELECT_FROM + " where " + PLACEHOLDER_SELECT_FROM; - Statement parsed = StringN1qlBasedQuery.prepare(statement, "B", "_class", String.class, false); + public void testReplaceAllFullSelectPlaceholder() throws Exception { + String statement = spel(SPEL_SELECT_FROM_CLAUSE) + " where " + spel(SPEL_SELECT_FROM_CLAUSE); + String parsed = mockStringUnderscoreClass.parseSpel(statement, false, new Object[0]); assertEquals("SELECT META(`B`).id AS _ID, META(`B`).cas AS _CAS, `B`.* FROM `B` where " - + PLACEHOLDER_SELECT_FROM, parsed.toString()); + + "SELECT META(`B`).id AS _ID, META(`B`).cas AS _CAS, `B`.* FROM `B`", parsed); } @Test public void testReplaceAllBucketPlaceholder() throws Exception { - String statement = "SELECT * FROM " + PLACEHOLDER_BUCKET + " WHERE " + PLACEHOLDER_BUCKET + ".test = 1"; - Statement parsed = StringN1qlBasedQuery.prepare(statement, "B", "_class", String.class, false); + String statement = "SELECT * FROM " + spel(SPEL_BUCKET) + " WHERE " + spel(SPEL_BUCKET) + ".test = 1"; + String parsed = mockStringUnderscoreClass.parseSpel(statement, false, new Object[0]); - assertEquals("SELECT * FROM `B` WHERE `B`.test = 1", parsed.toString()); + assertEquals("SELECT * FROM `B` WHERE `B`.test = 1", parsed); } @Test - public void testReplaceFirstEntityPlaceholder() throws Exception { - String statement = "SELECT " + PLACEHOLDER_ENTITY + " FROM b where b.test = 1 and " + PLACEHOLDER_ENTITY; - Statement parsed = StringN1qlBasedQuery.prepare(statement, "A", "_class", String.class, false); + public void testReplaceAllEntityPlaceholder() throws Exception { + String statement = "SELECT " + spel(SPEL_ENTITY) + " FROM a where a.test = 1 and " + spel(SPEL_ENTITY); + String parsed = mockStringUnderscoreClass.parseSpel(statement, false, new Object[0]); - assertEquals("SELECT META(`A`).id AS _ID, META(`A`).cas AS _CAS FROM b where b.test = 1 and " - + PLACEHOLDER_ENTITY, parsed.toString()); + assertEquals("SELECT META(`B`).id AS _ID, META(`B`).cas AS _CAS FROM a where a.test = 1 and " + + "META(`B`).id AS _ID, META(`B`).cas AS _CAS", parsed); } @Test public void testReplaceTypePlaceholder() throws Exception { - String statement = "SELECT " + PLACEHOLDER_ENTITY + " FROM b WHERE b.test = 1 AND " + PLACEHOLDER_FILTER_TYPE; - Statement parsed = StringN1qlBasedQuery.prepare(statement, "A", "@class", String.class, false); + String statement = "SELECT " + spel(SPEL_ENTITY) + " FROM a WHERE a.test = 1 AND " + spel(SPEL_FILTER); + String parsed = mockStringAtClass.parseSpel(statement, false, new Object[0]); - assertEquals("SELECT META(`A`).id AS _ID, META(`A`).cas AS _CAS FROM b WHERE b.test = 1 AND `@class` = " - + "\"java.lang.String\"", parsed.toString()); + assertEquals("SELECT META(`B`).id AS _ID, META(`B`).cas AS _CAS FROM a WHERE a.test = 1 AND `@class` = " + + "\"java.lang.String\"", parsed); } @Test public void testReplaceSelectFromPlaceholderWithCountIfCountTrue() { - String statement = PLACEHOLDER_SELECT_FROM + " WHERE true"; - Statement parsed = StringN1qlBasedQuery.prepare(statement, "A", "_class", String.class, true); + String statement = spel(SPEL_SELECT_FROM_CLAUSE) + " WHERE true"; + String parsed = mockStringUnderscoreClass.parseSpel(statement, true, new Object[0]); - assertEquals("SELECT COUNT(*) AS " + CountFragment.COUNT_ALIAS + " FROM `A` WHERE true", parsed.toString()); + assertEquals("SELECT COUNT(*) AS " + CountFragment.COUNT_ALIAS + " FROM `B` WHERE true", parsed); + } + + @Test + public void testParsingOfSpel() { + StringN1qlBasedQuery mock = mock(StringN1qlBasedQuery.class); + when(mock.parseSpel(anyString(), anyBoolean(), any(Object[].class))).thenCallRealMethod(); + when(mock.evaluateSpelExpression(anyString(), anyBoolean(), any(Object[].class))) + .thenReturn("EXPRESSION1", "EXPRESSION2isMuchMoreLongerThanTheTextItReplaces", "EXPRESSION3", "LASTEXPRESSION" + ); + + String spelStatement = "some statement with ${ad{\"toto\"}.lib} spel ${expression.parsed}${{several{close}}expressions}"; + String expected = "some statement with EXPRESSION1 spel EXPRESSION2isMuchMoreLongerThanTheTextItReplacesEXPRESSION3"; + + String parsed = mock.parseSpel(spelStatement, false, new Object[0]); + + assertEquals(expected, parsed); } } \ No newline at end of file diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index fcbb76c9..c3a47c8f 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -15,6 +15,10 @@ + + \ No newline at end of file