DATACOUCH-139 - Add limited support for @View query derivation.
@View can now trigger a basic query derivation. If viewName's explicitly set, query derivation is attempted. Otherwise, no derivation is made but instead the viewName is guessed by using the method name and removing a "find" or "count" prefix. Methods prefixed with "count" (and in general detected as count projections) will trigger a reduce on the view.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
* Copyright 2013-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.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.data.couchbase.repository;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -32,6 +33,7 @@ import org.springframework.test.context.support.DependencyInjectionTestExecution
|
||||
|
||||
/**
|
||||
* @author Michael Nitschinger
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
public class CouchbaseRepositoryViewListener extends DependencyInjectionTestExecutionListener {
|
||||
|
||||
@@ -51,8 +53,10 @@ public class CouchbaseRepositoryViewListener extends DependencyInjectionTestExec
|
||||
|
||||
private void createAndWaitForDesignDocs(final Bucket client) {
|
||||
String mapFunction = "function (doc, meta) { if(doc._class == \"org.springframework.data.couchbase.repository.User\") { emit(null, null); } }";
|
||||
String mapFunctionName = "function (doc, meta) { if(doc._class == \"org.springframework.data.couchbase.repository.User\") { emit(doc.username, null); } }";
|
||||
View view = DefaultView.create("customFindAllView", mapFunction, "_count");
|
||||
List<View> views = Collections.singletonList(view);
|
||||
View customFindByNameView = DefaultView.create("customFindByNameView", mapFunctionName, "_count");
|
||||
List<View> views = Arrays.asList(view, customFindByNameView);
|
||||
DesignDocument designDoc = DesignDocument.create("user", views);
|
||||
client.bucketManager().upsertDesignDocument(designDoc);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
* Copyright 2013-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.
|
||||
@@ -18,8 +18,10 @@ package org.springframework.data.couchbase.repository;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.couchbase.client.java.Bucket;
|
||||
import com.couchbase.client.java.view.Stale;
|
||||
@@ -33,12 +35,14 @@ import org.springframework.dao.InvalidDataAccessResourceUsageException;
|
||||
import org.springframework.data.couchbase.IntegrationTestApplicationConfig;
|
||||
import org.springframework.data.couchbase.core.CouchbaseTemplate;
|
||||
import org.springframework.data.couchbase.repository.support.CouchbaseRepositoryFactory;
|
||||
import org.springframework.data.mapping.PropertyReferenceException;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.TestExecutionListeners;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
/**
|
||||
* @author David Harrigan
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration(classes = IntegrationTestApplicationConfig.class)
|
||||
@@ -77,13 +81,89 @@ public class CouchbaseRepositoryViewTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldTrimOffFindOnCustomFinder() {
|
||||
public void shouldDetectMethodNameWithoutPropertyAndIssueGenericQueryOnView() {
|
||||
Iterable<User> users = repository.findRandomMethodName();
|
||||
assertNotNull(users);
|
||||
assertTrue(users.iterator().hasNext());
|
||||
|
||||
try {
|
||||
repository.findAllSomething();
|
||||
repository.findIncorrectExplicitView();
|
||||
fail("Expected InvalidDataAccessResourceException");
|
||||
} catch (InvalidDataAccessResourceUsageException e) {
|
||||
assertTrue(e.getMessage(), e.getMessage().startsWith("View user/allSomething does not exist"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = PropertyReferenceException.class)
|
||||
public void shouldFailDeriveOnBadProperty() {
|
||||
repository.findAllByUsernameEqualAndUserblablaIs("uname-1", "blabla");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDeriveViewParametersAndReduce() {
|
||||
long count = repository.countByUsernameGreaterThanEqualAndUsernameLessThan("uname-8", "uname-9");
|
||||
assertEquals(12, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDeriveViewParameters() {
|
||||
String lowKey = "uname-1";
|
||||
String middleKey = "uname-10";
|
||||
String highKey = "uname-11";
|
||||
List<String> keys = Arrays.asList(lowKey, middleKey, highKey);
|
||||
|
||||
User u1 = repository.findByUsernameIs(lowKey);
|
||||
User u2 = repository.findByUsernameIs(middleKey);
|
||||
User u3 = repository.findByUsernameIs(highKey);
|
||||
|
||||
|
||||
List<User> in = repository.findAllByUsernameIn(keys);
|
||||
List<User> gteLte = repository.findByUsernameGreaterThanEqualAndUsernameLessThanEqual(lowKey, highKey);
|
||||
List<User> between = repository.findByUsernameBetween(lowKey, highKey);
|
||||
List<User> gteLimited = repository.findTop3ByUsernameGreaterThanEqual(lowKey);
|
||||
|
||||
assertNotNull(u1);
|
||||
assertNotNull(u2);
|
||||
assertNotNull(u3);
|
||||
|
||||
assertEquals(lowKey, u1.getUsername());
|
||||
assertEquals(middleKey, u2.getUsername());
|
||||
assertEquals(highKey, u3.getUsername());
|
||||
|
||||
List<User> expected = Arrays.asList(u1, u2, u3);
|
||||
assertEquals(expected, in);
|
||||
assertEquals(expected, gteLte);
|
||||
assertEquals(expected, between);
|
||||
assertEquals(expected, gteLimited);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDeriveToEmptyClause() {
|
||||
List<User> users = repository.findAllByUsername();
|
||||
assertNotNull(users);
|
||||
assertEquals(100, users.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDetermineViewNameFromMethodPrefix() {
|
||||
try {
|
||||
repository.findByIncorrectView();
|
||||
fail("Expected InvalidDataAccessResourceException");
|
||||
} catch (InvalidDataAccessResourceUsageException e) {
|
||||
assertTrue(e.getMessage(), e.getMessage().startsWith("View user/byIncorrectView does not exist"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDetermineViewNameFromCountPrefixAndReduce() {
|
||||
long count = repository.countCustomFindAllView();
|
||||
assertEquals(100, count);
|
||||
|
||||
try {
|
||||
repository.countCustomFindInvalid();
|
||||
fail("Expected InvalidDataAccessResourceException");
|
||||
} catch (InvalidDataAccessResourceUsageException e) {
|
||||
assertTrue(e.getMessage(), e.getMessage().startsWith("View user/customFindInvalid does not exist"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
* Copyright 2013-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.
|
||||
@@ -16,10 +16,13 @@
|
||||
|
||||
package org.springframework.data.couchbase.repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.couchbase.core.view.View;
|
||||
|
||||
/**
|
||||
* @author David Harrigan
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
public interface CustomUserRepository extends CouchbaseRepository<User, String> {
|
||||
|
||||
@@ -31,7 +34,42 @@ public interface CustomUserRepository extends CouchbaseRepository<User, String>
|
||||
@View(designDocument = "userCustom", viewName = "customCountView")
|
||||
long count();
|
||||
|
||||
@View
|
||||
Iterable<User> findAllSomething();
|
||||
@View(viewName = "allSomething")
|
||||
Iterable<User> findIncorrectExplicitView();
|
||||
|
||||
@View(viewName = "customFindAllView")
|
||||
Iterable<User> findRandomMethodName();
|
||||
|
||||
@View(viewName = "customFindByNameView")
|
||||
long countByUsernameGreaterThanEqualAndUsernameLessThan(String lowBound, String highBound);
|
||||
|
||||
@View(viewName = "customFindByNameView")
|
||||
User findByUsernameIs(String lowKey);
|
||||
|
||||
@View(viewName = "customFindByNameView")
|
||||
List<User> findAllByUsernameIn(List<String> keys);
|
||||
|
||||
@View(viewName = "customFindByNameView")
|
||||
List<User> findByUsernameGreaterThanEqualAndUsernameLessThanEqual(String lowKey, String highKey);
|
||||
|
||||
@View(viewName = "customFindByNameView")
|
||||
List<User> findByUsernameBetween(String lowKey, String highKey);
|
||||
|
||||
@View(viewName = "customFindByNameView")
|
||||
List<User> findTop3ByUsernameGreaterThanEqual(String lowKey);
|
||||
|
||||
@View(viewName = "customFindAllView")
|
||||
List<User> findAllByUsername();
|
||||
|
||||
@View(viewName = "customFindAllView")
|
||||
List<User> findAllByUsernameEqualAndUserblablaIs(String s, String blabla);
|
||||
|
||||
@View
|
||||
List<User> findByIncorrectView();
|
||||
|
||||
@View
|
||||
long countCustomFindAllView();
|
||||
|
||||
@View
|
||||
long countCustomFindInvalid();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
* Copyright 2013-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.
|
||||
@@ -20,6 +20,7 @@ import org.springframework.data.annotation.Id;
|
||||
|
||||
/**
|
||||
* @author Michael Nitschinger
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
public class User {
|
||||
|
||||
@@ -41,4 +42,21 @@ public class User {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
User user = (User) o;
|
||||
return key.equals(user.key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return key.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
* Copyright 2013-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.
|
||||
@@ -26,6 +26,7 @@ import java.lang.annotation.Target;
|
||||
* Annotation to support the use of Views with Couchbase.
|
||||
*
|
||||
* @author David Harrigan.
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
@Documented
|
||||
@Target(ElementType.METHOD)
|
||||
@@ -33,18 +34,16 @@ import java.lang.annotation.Target;
|
||||
public @interface View {
|
||||
|
||||
/**
|
||||
* The name of the Design Document to use.
|
||||
* <p/>
|
||||
* If the {@link #viewName()} field is set, this field is mandatory.
|
||||
* The name of the Design Document to use. If omitted, defaults to one derived from the entity class name.
|
||||
*
|
||||
* @return name of the Design Document.
|
||||
*/
|
||||
String designDocument() default "";
|
||||
|
||||
/**
|
||||
* The name of the View to use.
|
||||
* <p/>
|
||||
* If the {@link #designDocument()} field is set, his field is mandatory.
|
||||
* The name of the View to use. If omitted, defaults to one derived from the method name (stripped of prefix "find" or
|
||||
* "count"). This is mandatory to trigger a query derivation from the method name (ie. a View query with parameters
|
||||
* like limit, startkey, etc...).
|
||||
*
|
||||
* @return name of the View
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013, 2014 the original author or authors.
|
||||
* Copyright 2013-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.
|
||||
@@ -63,11 +63,33 @@ public class CouchbaseQueryMethod extends QueryMethod {
|
||||
* @return true if it has the annotation and full view specified.
|
||||
*/
|
||||
public boolean hasViewSpecification() {
|
||||
return hasDesignDoc() && hasViewName();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the method has a @View annotation with the designDocument specified.
|
||||
*
|
||||
* @return true if it has the design document specified.
|
||||
*/
|
||||
public boolean hasDesignDoc() {
|
||||
View annotation = getViewAnnotation();
|
||||
if (annotation == null) {
|
||||
return false;
|
||||
}
|
||||
return StringUtils.hasText(annotation.designDocument()) && StringUtils.hasText(annotation.viewName());
|
||||
return StringUtils.hasText(annotation.designDocument());
|
||||
}
|
||||
|
||||
/**
|
||||
* If the method has a @View annotation with the viewName specified.
|
||||
*
|
||||
* @return true if it has the view name specified.
|
||||
*/
|
||||
public boolean hasViewName() {
|
||||
View annotation = getViewAnnotation();
|
||||
if (annotation == null) {
|
||||
return false;
|
||||
}
|
||||
return StringUtils.hasText(annotation.viewName());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013, 2014 the original author or authors.
|
||||
* Copyright 2013-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.
|
||||
@@ -16,17 +16,27 @@
|
||||
|
||||
package org.springframework.data.couchbase.repository.query;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.couchbase.client.java.document.json.JsonObject;
|
||||
import com.couchbase.client.java.view.ViewQuery;
|
||||
import com.couchbase.client.java.view.ViewResult;
|
||||
import com.couchbase.client.java.view.ViewRow;
|
||||
|
||||
import org.springframework.data.couchbase.core.CouchbaseOperations;
|
||||
import org.springframework.data.couchbase.core.CouchbaseQueryExecutionException;
|
||||
import org.springframework.data.mapping.PropertyReferenceException;
|
||||
import org.springframework.data.repository.query.ParametersParameterAccessor;
|
||||
import org.springframework.data.repository.query.QueryMethod;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Execute a repository query through the View mechanism.
|
||||
*
|
||||
* @author Michael Nitschinger
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
public class ViewBasedCouchbaseQuery implements RepositoryQuery {
|
||||
|
||||
@@ -40,24 +50,73 @@ public class ViewBasedCouchbaseQuery implements RepositoryQuery {
|
||||
|
||||
@Override
|
||||
public Object execute(Object[] runtimeParams) {
|
||||
ViewQuery query = null;
|
||||
for (Object param : runtimeParams) {
|
||||
if (param instanceof ViewQuery) {
|
||||
query = (ViewQuery) param;
|
||||
//FIXME clone the ViewQuery and use the @View design / viewname
|
||||
if (method.hasViewName()) { //only allow derivation on @View explicitly defining a viewName
|
||||
return deriveAndExecute(runtimeParams);
|
||||
} else {
|
||||
return guessViewAndExecute();
|
||||
}
|
||||
}
|
||||
|
||||
protected Object guessViewAndExecute() {
|
||||
String designDoc = designDocName(method);
|
||||
String methodName = method.getName();
|
||||
boolean isReduce = methodName.startsWith("count");
|
||||
String viewName = StringUtils.uncapitalize(methodName.replaceFirst("find|count", ""));
|
||||
|
||||
ViewQuery simpleQuery = ViewQuery.from(designDoc, viewName);
|
||||
if (isReduce) {
|
||||
simpleQuery.reduce(isReduce);
|
||||
return executeReduce(simpleQuery, designDoc, viewName);
|
||||
} else {
|
||||
return execute(simpleQuery);
|
||||
}
|
||||
}
|
||||
|
||||
protected Object deriveAndExecute(Object[] runtimeParams) {
|
||||
String designDoc = designDocName(method);
|
||||
String viewName = method.getViewAnnotation().viewName();
|
||||
ViewQuery baseQuery = ViewQuery.from(designDoc, viewName);
|
||||
try {
|
||||
PartTree tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType());
|
||||
|
||||
ViewQueryCreator creator = new ViewQueryCreator(tree,
|
||||
new ParametersParameterAccessor(method.getParameters(), runtimeParams),
|
||||
baseQuery);
|
||||
|
||||
ViewQuery query = creator.createQuery();
|
||||
|
||||
if (tree.isCountProjection() == Boolean.TRUE) {
|
||||
return executeReduce(query, designDoc, viewName);
|
||||
} else {
|
||||
throw new IllegalStateException("Unknown query param: " + param);
|
||||
return execute(query);
|
||||
}
|
||||
} catch (PropertyReferenceException e) {
|
||||
if (e.getPropertyName().equals(method.getName())) {
|
||||
return execute(baseQuery);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (query == null) {
|
||||
query = ViewQuery.from(designDocName(), viewName());
|
||||
}
|
||||
query.reduce(false);
|
||||
|
||||
protected Object execute(ViewQuery query) {
|
||||
return operations.findByView(query, method.getEntityInformation().getJavaType());
|
||||
}
|
||||
|
||||
protected Object executeReduce(ViewQuery query, String designDoc, String viewName) {
|
||||
ViewResult viewResult = operations.queryView(query);
|
||||
List<ViewRow> allRows = viewResult.allRows();
|
||||
JsonObject error = viewResult.error();
|
||||
if (error != null) {
|
||||
throw new CouchbaseQueryExecutionException("Error while reducing on view " + designDoc + "/" + viewName +
|
||||
": " + error);
|
||||
}
|
||||
if (allRows == null || allRows.isEmpty()) {
|
||||
return null;
|
||||
} else{
|
||||
return allRows.get(0).value();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryMethod getQueryMethod() {
|
||||
return method;
|
||||
@@ -68,7 +127,7 @@ public class ViewBasedCouchbaseQuery implements RepositoryQuery {
|
||||
*
|
||||
* @return the design document name.
|
||||
*/
|
||||
private String designDocName() {
|
||||
private static String designDocName(CouchbaseQueryMethod method) {
|
||||
if (method.hasViewSpecification()) {
|
||||
return method.getViewAnnotation().designDocument();
|
||||
} else if (method.hasViewAnnotation()) {
|
||||
@@ -78,19 +137,4 @@ public class ViewBasedCouchbaseQuery implements RepositoryQuery {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the best-guess view name.
|
||||
*
|
||||
* @return the view name.
|
||||
*/
|
||||
private String viewName() {
|
||||
if (method.hasViewSpecification()) {
|
||||
return method.getViewAnnotation().viewName();
|
||||
} else if (method.hasViewAnnotation()) {
|
||||
return StringUtils.uncapitalize(method.getName().replaceFirst("find", ""));
|
||||
} else {
|
||||
throw new IllegalStateException("View-based query should only happen on a method with @View annotation");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* 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.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.couchbase.client.java.document.json.JsonArray;
|
||||
import com.couchbase.client.java.document.json.JsonObject;
|
||||
import com.couchbase.client.java.view.ViewQuery;
|
||||
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.repository.query.ParameterAccessor;
|
||||
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
|
||||
import org.springframework.data.repository.query.parser.Part;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
|
||||
/**
|
||||
* A QueryCreator that will enrich a {@link ViewQuery} using query derivation mechanisms
|
||||
* and the parsed {@link PartTree}.
|
||||
* <p>
|
||||
* Support for query derivation keywords is very limited (and especially you have to use one valid entity property name
|
||||
* in your query naming, and compound key views are not supported).
|
||||
* <br/>
|
||||
* Here are the {@link Part.Type} supported:
|
||||
* <ul>
|
||||
* <li><b>GREATER_THAN_EQUAL:</b> uses {@link ViewQuery#startKey(String) startkey}</li>
|
||||
* <li><b>LESS_THAN_EQUAL:</b> uses {@link ViewQuery#endKey(String) endkey} {@link ViewQuery#inclusiveEnd() inclusive}</li>
|
||||
* <li><b>LESS_THAN / BEFORE:</b> uses {@link ViewQuery#endKey(String) endkey}</li>
|
||||
* <li><b>BETWEEN:</b> uses {@link ViewQuery#startKey(String) startkey},
|
||||
* {@link ViewQuery#endKey(String) endkey(exclusive)}</li>
|
||||
* <li><b>STARTING_WITH:</b> (only with String key). uses {@link ViewQuery#startKey(String) startkey},
|
||||
* {@link ViewQuery#endKey(String) endkey(exclusive)}. Will append special unicode char <code>\uefff</code>.</li>
|
||||
* <li><b>SIMPLE_PROPERTY:</b> (aka "Is", "Equals"). This one can have no argument if used alone
|
||||
* (eg. "findAllByUsername"), otherwise uses {@link ViewQuery#key(String) key}</li>
|
||||
* <li><b>IN:</b> uses {@link ViewQuery#keys(JsonArray) keys} (provide a collection or array)</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
* Additionally, {@link PartTree#isLimiting()} will use {@link ViewQuery#limit(int) limit}
|
||||
* and {@link PartTree#isCountProjection()} will trigger a {@link ViewQuery#reduce() reduce}.
|
||||
*/
|
||||
public class ViewQueryCreator extends AbstractQueryCreator<ViewQuery, ViewQuery> {
|
||||
|
||||
private ViewQuery query;
|
||||
private final PartTree tree;
|
||||
private final int treeCount;
|
||||
|
||||
public ViewQueryCreator(PartTree tree, ParameterAccessor parameters, ViewQuery query) {
|
||||
super(tree, parameters);
|
||||
this.query = query;
|
||||
this.tree = tree;
|
||||
|
||||
//sanity check the partTree since we have strong restrictions on what's supported:
|
||||
int i = 0;
|
||||
Set<String> properties = new HashSet<String>();
|
||||
for (PartTree.OrPart parts : tree) {
|
||||
for (Part part : parts) {
|
||||
i++;
|
||||
properties.add(part.getProperty().toDotPath());
|
||||
}
|
||||
}
|
||||
this.treeCount = i;
|
||||
if (properties.size() > 1) {
|
||||
throw new IllegalArgumentException("View-based queries do not support compound keys");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewQuery create(Part part, Iterator<Object> iterator) {
|
||||
switch (part.getType()) {
|
||||
case GREATER_THAN_EQUAL:
|
||||
startKey(iterator);
|
||||
break;
|
||||
case LESS_THAN_EQUAL:
|
||||
query.inclusiveEnd(true);
|
||||
case BEFORE:
|
||||
case LESS_THAN://fall-through on purpose here
|
||||
endKey(iterator);
|
||||
break;
|
||||
case BETWEEN:
|
||||
startKey(iterator);
|
||||
endKey(iterator);
|
||||
break;
|
||||
case STARTING_WITH: //starting_with only supports String keys
|
||||
String nameStart = nextString(iterator);
|
||||
query.startKey(nameStart).endKey(nameStart + "\uefff");
|
||||
query.inclusiveEnd(false);
|
||||
break;
|
||||
case SIMPLE_PROPERTY:
|
||||
key(iterator);
|
||||
break;
|
||||
case IN:
|
||||
query.keys(in(iterator));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported keyword in View query derivation: " + part.toString());
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
private void startKey(Iterator<Object> iterator) {
|
||||
if (!iterator.hasNext()) {
|
||||
throw new IllegalArgumentException("Not enough parameters for startKey");
|
||||
}
|
||||
|
||||
Object next = iterator.next();
|
||||
if (next instanceof String) {
|
||||
query.startKey((String) next);
|
||||
} else if (next instanceof Boolean) {
|
||||
query.startKey((Boolean) next);
|
||||
} else if (next instanceof Double) {
|
||||
query.startKey((Double) next);
|
||||
} else if (next instanceof Integer) {
|
||||
query.startKey((Integer) next);
|
||||
} else if (next instanceof Long) {
|
||||
query.startKey((Long) next);
|
||||
} else if (next instanceof Collection) {
|
||||
//when creating a JsonArray, the from(List) method is preferred because it will convert internal
|
||||
//Lists and Maps to JsonObject and JsonArray respectively
|
||||
List<Object> arrayContent = new ArrayList<Object>((Collection) next);
|
||||
query.startKey(JsonArray.from(arrayContent));
|
||||
} else if (next.getClass().isArray()) {
|
||||
List<Object> arrayContent = Arrays.asList((Object[]) next);
|
||||
query.startKey(JsonArray.from(arrayContent));
|
||||
} else if (next instanceof JsonArray) { //discouraged, since it's leaking store-specifics in the method signature
|
||||
query.startKey((JsonArray) next);
|
||||
} else if (next instanceof JsonObject) { //discouraged, since it's leaking store-specifics in the method signature
|
||||
query.startKey((JsonObject) next);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported parameter type for startKey: " + next.getClass());
|
||||
}
|
||||
}
|
||||
private void endKey(Iterator<Object> iterator) {
|
||||
if (!iterator.hasNext()) {
|
||||
throw new IllegalArgumentException("Not enough parameters for endKey");
|
||||
}
|
||||
|
||||
Object next = iterator.next();
|
||||
if (next instanceof String) {
|
||||
query.endKey((String) next);
|
||||
} else if (next instanceof Boolean) {
|
||||
query.endKey((Boolean) next);
|
||||
} else if (next instanceof Double) {
|
||||
query.endKey((Double) next);
|
||||
} else if (next instanceof Integer) {
|
||||
query.endKey((Integer) next);
|
||||
} else if (next instanceof Long) {
|
||||
query.endKey((Long) next);
|
||||
} else if (next instanceof Collection) {
|
||||
//when creating a JsonArray, the from(List) method is preferred because it will convert internal
|
||||
//Lists and Maps to JsonObject and JsonArray respectively
|
||||
List<Object> arrayContent = new ArrayList<Object>((Collection) next);
|
||||
query.endKey(JsonArray.from(arrayContent));
|
||||
} else if (next.getClass().isArray()) {
|
||||
List<Object> arrayContent = Arrays.asList((Object[]) next);
|
||||
query.endKey(JsonArray.from(arrayContent));
|
||||
} else if (next instanceof JsonArray) { //discouraged, since it's leaking store-specifics in the method signature
|
||||
query.endKey((JsonArray) next);
|
||||
} else if (next instanceof JsonObject) { //discouraged, since it's leaking store-specifics in the method signature
|
||||
query.endKey((JsonObject) next);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported parameter type for endKey: " + next.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
private void key(Iterator<Object> iterator) {
|
||||
if (!iterator.hasNext() && treeCount > 1) {
|
||||
throw new IllegalArgumentException("Not enough parameters for key");
|
||||
} else if (!iterator.hasNext()) {
|
||||
//probably pattern like findAllByUsername(), just apply query without parameters
|
||||
return;
|
||||
}
|
||||
|
||||
Object next = iterator.next();
|
||||
if (next instanceof String) {
|
||||
query.key((String) next);
|
||||
} else if (next instanceof Boolean) {
|
||||
query.key((Boolean) next);
|
||||
} else if (next instanceof Double) {
|
||||
query.key((Double) next);
|
||||
} else if (next instanceof Integer) {
|
||||
query.key((Integer) next);
|
||||
} else if (next instanceof Long) {
|
||||
query.key((Long) next);
|
||||
} else if (next instanceof Collection) {
|
||||
//when creating a JsonArray, the from(List) method is preferred because it will convert internal
|
||||
//Lists and Maps to JsonObject and JsonArray respectively
|
||||
List<Object> arrayContent = new ArrayList<Object>((Collection) next);
|
||||
query.key(JsonArray.from(arrayContent));
|
||||
} else if (next.getClass().isArray()) {
|
||||
List<Object> arrayContent = Arrays.asList((Object[]) next);
|
||||
query.key(JsonArray.from(arrayContent));
|
||||
} else if (next instanceof JsonArray) { //discouraged, since it's leaking store-specifics in the method signature
|
||||
query.key((JsonArray) next);
|
||||
} else if (next instanceof JsonObject) { //discouraged, since it's leaking store-specifics in the method signature
|
||||
query.key((JsonObject) next);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported parameter type for key: " + next.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private String nextString(Iterator<Object> iterator) {
|
||||
if (!iterator.hasNext()) {
|
||||
throw new IllegalStateException("Not enough parameters");
|
||||
}
|
||||
|
||||
Object next = iterator.next();
|
||||
if (!(next instanceof String)) {
|
||||
throw new IllegalArgumentException("Expected String, got " + next.getClass().getName());
|
||||
}
|
||||
|
||||
return (String) next;
|
||||
}
|
||||
|
||||
private JsonArray in(Iterator<Object> iterator) {
|
||||
if (!iterator.hasNext()) {
|
||||
throw new IllegalArgumentException("Not enough parameters for in");
|
||||
}
|
||||
Object next = iterator.next();
|
||||
|
||||
List<Object> values;
|
||||
if (next instanceof Collection) {
|
||||
values = new ArrayList<Object>((Collection<?>) next);
|
||||
} else if (next.getClass().isArray()) {
|
||||
values = Arrays.asList((Object[]) next);
|
||||
} else {
|
||||
values = Collections.singletonList(next);
|
||||
}
|
||||
//using the from(List) method ensure that contained Lists and Maps will be converted to JsonArrays and JsonObjects
|
||||
return JsonArray.from(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewQuery and(Part part, ViewQuery base, Iterator<Object> iterator) {
|
||||
return create(part, iterator);//and not really supported, all query derivation mutate the original ViewQuery
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewQuery or(ViewQuery base, ViewQuery criteria) {
|
||||
//this won't be called unless there's a Or keyword in the method
|
||||
throw new UnsupportedOperationException("Or is not supported for View-based queries");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewQuery complete(ViewQuery criteria, Sort sort) {
|
||||
boolean descending = false;
|
||||
|
||||
if (sort != null) {
|
||||
int sortCount = 0;
|
||||
Iterator<Sort.Order> it = sort.iterator();
|
||||
while(it.hasNext()) {
|
||||
sortCount++;
|
||||
if (!it.next().isAscending()) {
|
||||
descending = true;
|
||||
}
|
||||
}
|
||||
if (sortCount > 1) {
|
||||
throw new IllegalArgumentException("Detected " + sortCount + " sort instructions, maximum one supported");
|
||||
}
|
||||
query.descending(descending);
|
||||
}
|
||||
|
||||
if (tree.isLimiting()) {
|
||||
query.limit(tree.getMaxResults());
|
||||
}
|
||||
|
||||
query.reduce(tree.isCountProjection() == Boolean.TRUE);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user