DATACOUCH-166 - Improve SpEL support, prefix is now #{
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -34,13 +34,13 @@ public interface UserRepository extends CouchbaseRepository<User, String> {
|
||||
@View(designDocument = "user", viewName = "all")
|
||||
Iterable<User> 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
|
||||
|
||||
@@ -122,7 +122,7 @@ Here is an example:
|
||||
----
|
||||
public interface UserRepository extends CrudRepository<UserInfo, String> {
|
||||
|
||||
@Query("${#n1ql.selectEntity} WHERE role = 'admin' AND ${#n1ql.filter}")
|
||||
@Query("#{#n1ql.selectEntity} WHERE role = 'admin' AND #{#n1ql.filter}")
|
||||
List<UserInfo> findAllAdmins();
|
||||
|
||||
List<UserInfo> findByFirstname(String fname);
|
||||
@@ -132,7 +132,7 @@ public interface UserRepository extends CrudRepository<UserInfo, String> {
|
||||
|
||||
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`...
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery;
|
||||
* <p/>
|
||||
* In this case, one can use a placeholder notation of {@code ?0}, {@code ?1} and so on.
|
||||
* <p/>
|
||||
* Also, SpEL in the form <code>${spelExpression}</code> is supported, including the
|
||||
* Also, SpEL in the form <code>#{spelExpression}</code> is supported, including the
|
||||
* following N1QL variables that will be replaced by the underlying {@link CouchbaseTemplate}
|
||||
* associated information:
|
||||
* <ul>
|
||||
|
||||
@@ -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. <code>name = $1</code>) that will map to the
|
||||
* method's parameters, in the same order.
|
||||
* <p/>
|
||||
* The statement can also contain SpEL expressions enclosed in <code>${}</code>.
|
||||
* <p/> There are couchbase-provided variables included for the {@link #SPEL_BUCKET bucket namespace},
|
||||
* The statement can also contain SpEL expressions enclosed in <code>#{</code> and <code>}</code>.
|
||||
* <p/>
|
||||
* 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 <code>SELECT x FROM y</code> clause needed
|
||||
* for entity mapping. Eg. <code>"${{@value StringN1qlBasedQuery#SPEL_SELECT_FROM_CLAUSE}} WHERE test = true"</code>.
|
||||
* for entity mapping. Eg. <code>"#{{@value StringN1qlBasedQuery#SPEL_SELECT_FROM_CLAUSE}} WHERE test = true"</code>.
|
||||
* 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. <code>"SELECT * FROM ${{@value StringN1qlBasedQuery#SPEL_BUCKET}} LIMIT 3"</code>.
|
||||
* entity. Eg. <code>"SELECT * FROM #{{@value StringN1qlBasedQuery#SPEL_BUCKET}} LIMIT 3"</code>.
|
||||
*/
|
||||
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. <code>"SELECT ${{@value StringN1qlBasedQuery#SPEL_ENTITY}} FROM test"</code>.
|
||||
* (SELECT clause). Eg. <code>"SELECT #{{@value StringN1qlBasedQuery#SPEL_ENTITY}} FROM test"</code>.
|
||||
*/
|
||||
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. <code>"SELECT * FROM test WHERE test = true AND ${{@value StringN1qlBasedQuery#SPEL_FILTER}}"</code>.
|
||||
* documents matching the entity's class. Eg. <code>"SELECT * FROM test WHERE test = true AND #{{@value StringN1qlBasedQuery#SPEL_FILTER}}"</code>.
|
||||
*/
|
||||
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 <code>${</code> and <code>}</code>)
|
||||
* and replace said expressions with their corresponding values (see {@link #evaluateSpelExpression(String, boolean, Object[])}).
|
||||
* Parse the statement to detect SPEL blocks (delimited by <code>#{</code> and <code>}</code>)
|
||||
* 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 <code>${</code> and <code>}</code>)
|
||||
* 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 {
|
||||
|
||||
/**
|
||||
* <code>${{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
|
||||
* <code>#{{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
|
||||
* selectEntity</code> will be replaced by the SELECT-FROM clause corresponding to the entity. Use once at the beginning.
|
||||
*/
|
||||
public final String selectEntity;
|
||||
|
||||
/**
|
||||
* <code>${{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
|
||||
* <code>#{{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
|
||||
* fields</code> will be replaced by the list of N1QL fields allowing to reconstruct the entity.
|
||||
*/
|
||||
public final String fields;
|
||||
|
||||
/**
|
||||
* <code>${{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
|
||||
* <code>#{{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
|
||||
* bucket</code> will be replaced by (escaped) bucket name in which the entity is stored.
|
||||
*/
|
||||
public final String bucket;
|
||||
|
||||
/**
|
||||
* <code>${{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
|
||||
* <code>#{{@value org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery#SPEL_PREFIX}.
|
||||
* filter</code> will be replaced by an expression allowing to select only entries matching the entity in a WHERE clause.
|
||||
*/
|
||||
public final String filter;
|
||||
|
||||
@@ -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<Object>() {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user