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:
Simon Baslé
2015-07-15 17:04:15 +02:00
parent 4920acc214
commit 4b453d4246
8 changed files with 545 additions and 48 deletions

View File

@@ -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);

View File

@@ -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"));
}
}
}

View File

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

View File

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

View File

@@ -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
*/

View File

@@ -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());
}
/**

View File

@@ -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");
}
}
}

View File

@@ -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;
}
}