DATACOUCH-166 - Add basic SpEL support

Added the step of parsing SpEL expressions delimited by ${ } in StringN1qlBasedQuery, then the actual evaluation of each expression.

Refactored StringN1qlBasedQuery to use SpEL across the board, including for N1QL specific placeholders (SELECT-FROM, WHERE filter, etc...).

Refactored StringN1qlBasedQuery to use local methods rather than static methods, and amended the unit tests accordingly (using mocks).

Refactored getStatement and getCountStatement so they can see the parameters passed in at runtime.

Added unit tests + integration test.

Added SpEL support example in documentation.
This commit is contained in:
Simon Baslé
2015-10-16 16:51:31 +02:00
parent 6294b03f46
commit ec129e86a8
10 changed files with 324 additions and 91 deletions

View File

@@ -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<Object>() {
@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);
}
}

View File

@@ -15,6 +15,10 @@
<appender-ref ref="console" />
</root>
<!-- You can use these logger configurations to log more details:
- log the Couchbase queries produced by the framework
- log details of the parsing of SpEL in N1QL inline queries-->
<logger name="org.springframework.data.couchbase.repository.query" level="debug"/>
<logger name="org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery" level="trace"/>
</configuration>