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 f845d78f..937c5dda 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java @@ -136,8 +136,8 @@ public class SimpleCouchbaseRepositoryTests { User user = repository.findByUsernameBadSelect("uname-1"); fail("shouldFailFindByUsernameWithNoIdOrCas"); } catch (CouchbaseQueryExecutionException e) { - assertTrue(e.getMessage().contains("_ID")); - assertTrue(e.getMessage().contains("_CAS")); + assertTrue("_ID expected in exception " + e, e.getMessage().contains("_ID")); + assertTrue("_CAS expected in exception " + e, e.getMessage().contains("_CAS")); } catch (Exception e) { fail("CouchbaseQueryExecutionException expected"); } 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 794130c7..4b2752c4 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java @@ -34,13 +34,13 @@ public interface UserRepository extends CouchbaseRepository { @View(designDocument = "user", viewName = "all") Iterable customViewQuery(ViewQuery query); - @Query("${#n1ql.selectEntity} WHERE username = $1") + @Query("#{#n1ql.selectEntity} WHERE username = $1") User findByUsername(String username); - @Query("SELECT * FROM ${#n1ql.bucket} WHERE username = $1") + @Query("SELECT * FROM #{#n1ql.bucket} WHERE username = $1") User findByUsernameBadSelect(String username); - @Query("${#n1ql.selectEntity} WHERE username LIKE '%-${3 + 1}'") + @Query("#{#n1ql.selectEntity} WHERE username LIKE '%-#{3 + 1}'") User findByUsernameWithSpelAndPlaceholder(); @Query diff --git a/src/main/asciidoc/repository.adoc b/src/main/asciidoc/repository.adoc index 2938dbe6..418f4132 100644 --- a/src/main/asciidoc/repository.adoc +++ b/src/main/asciidoc/repository.adoc @@ -122,7 +122,7 @@ Here is an example: ---- public interface UserRepository extends CrudRepository { - @Query("${#n1ql.selectEntity} WHERE role = 'admin' AND ${#n1ql.filter}") + @Query("#{#n1ql.selectEntity} WHERE role = 'admin' AND #{#n1ql.filter}") List findAllAdmins(); List findByFirstname(String fname); @@ -132,7 +132,7 @@ public interface UserRepository extends CrudRepository { Here we see two N1QL-backed ways of querying. -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`). +The first method uses the `Query` annotation to provide a N1QL statement inline. SpEL (Spring Expression Language) is supported by surrounding SpEL expression blocks between `#{` and `}`. You can also include N1QL positional placeholders (eg. `$1`, `$2`). A few N1QL-specific values are provided through SpEL: - `#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). @@ -140,8 +140,8 @@ A few N1QL-specific values are provided through SpEL: - `#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. -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`". +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`... diff --git a/src/main/java/org/springframework/data/couchbase/core/query/Query.java b/src/main/java/org/springframework/data/couchbase/core/query/Query.java index b7bc9793..1249625d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/Query.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/Query.java @@ -34,7 +34,7 @@ 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, SpEL in the form ${spelExpression} is supported, including the + * Also, SpEL in the form #{spelExpression} is supported, including the * following N1QL variables that will be replaced by the underlying {@link CouchbaseTemplate} * associated information: *

    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 33bab19d..c243ca6a 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 @@ -28,6 +28,7 @@ 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; /** @@ -36,8 +37,9 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; * 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 ${}. - *

    There are couchbase-provided variables included for the {@link #SPEL_BUCKET bucket namespace}, + * 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. @@ -54,7 +56,7 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery { /** * 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". + * 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"; @@ -62,21 +64,21 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery { /** * 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". + * 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". + * (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}}". + * 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"; @@ -123,81 +125,28 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery { } /** - * Parse the statement to detect SPEL expressions (delimited by ${ and }) - * and replace said expressions with their corresponding values (see {@link #evaluateSpelExpression(String, boolean, Object[])}). + * 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) { - 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)); - } - 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(); - } - - /** - * 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 doEvaluate(evaluationContext, parsedExpression, n1qlSpelValues); + return doParse(statement, parser, evaluationContext, n1qlSpelValues); } - protected static String doEvaluate(EvaluationContext context, Expression parsedExpression, N1qlSpelValues spelValues) { - context.setVariable(SPEL_PREFIX, spelValues); - return parsedExpression.getValue(context, String.class); + //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 @@ -228,25 +177,25 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery { public static final class N1qlSpelValues { /** - * ${{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}. + * #{{@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}. + * #{{@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}. + * #{{@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}. + * #{{@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; 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 9ee26522..8bb5cd78 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 @@ -12,13 +12,14 @@ 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 static final EvaluationContext SPEL_EVALUATION_CONTEXT = new StandardEvaluationContext(); + private StringN1qlBasedQuery mockStringUnderscoreClass; private StringN1qlBasedQuery mockStringAtClass; @@ -30,33 +31,30 @@ public class StringN1QlBasedQueryTest { 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))) + when(mockStringUnderscoreClass.parseSpel(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)); + when(mockStringAtClass.parseSpel(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]); + String statement = (String) invocation.getArguments()[0]; boolean isCount = (Boolean) invocation.getArguments()[1]; if (isCount) - return doEvaluate(context, expression, countSpelValues); + return doParse(statement, SPEL_PARSER, SPEL_EVALUATION_CONTEXT, countSpelValues); else - return doEvaluate(context, expression, spelValues); + return doParse(statement, SPEL_PARSER, SPEL_EVALUATION_CONTEXT, spelValues); } }; } private static String spel(String expression) { - return "${" + expression + "}"; + return "#{" + expression + "}"; } @Test @@ -101,20 +99,4 @@ public class StringN1QlBasedQueryTest { 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