From 4b453d424639ec7f9996406ce111baa84fbd16dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 15 Jul 2015 17:04:15 +0200 Subject: [PATCH] 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. --- .../CouchbaseRepositoryViewListener.java | 8 +- .../CouchbaseRepositoryViewTests.java | 90 +++++- .../repository/CustomUserRepository.java | 44 ++- .../data/couchbase/repository/User.java | 20 +- .../data/couchbase/core/view/View.java | 13 +- .../query/CouchbaseQueryMethod.java | 26 +- .../query/ViewBasedCouchbaseQuery.java | 100 ++++-- .../repository/query/ViewQueryCreator.java | 292 ++++++++++++++++++ 8 files changed, 545 insertions(+), 48 deletions(-) create mode 100644 src/main/java/org/springframework/data/couchbase/repository/query/ViewQueryCreator.java diff --git a/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewListener.java b/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewListener.java index 13420e40..a99e8709 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewListener.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewListener.java @@ -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 views = Collections.singletonList(view); + View customFindByNameView = DefaultView.create("customFindByNameView", mapFunctionName, "_count"); + List views = Arrays.asList(view, customFindByNameView); DesignDocument designDoc = DesignDocument.create("user", views); client.bucketManager().upsertDesignDocument(designDoc); diff --git a/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewTests.java b/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewTests.java index ef603cdb..d4317f54 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryViewTests.java @@ -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 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 keys = Arrays.asList(lowKey, middleKey, highKey); + + User u1 = repository.findByUsernameIs(lowKey); + User u2 = repository.findByUsernameIs(middleKey); + User u3 = repository.findByUsernameIs(highKey); + + + List in = repository.findAllByUsernameIn(keys); + List gteLte = repository.findByUsernameGreaterThanEqualAndUsernameLessThanEqual(lowKey, highKey); + List between = repository.findByUsernameBetween(lowKey, highKey); + List gteLimited = repository.findTop3ByUsernameGreaterThanEqual(lowKey); + + assertNotNull(u1); + assertNotNull(u2); + assertNotNull(u3); + + assertEquals(lowKey, u1.getUsername()); + assertEquals(middleKey, u2.getUsername()); + assertEquals(highKey, u3.getUsername()); + + List expected = Arrays.asList(u1, u2, u3); + assertEquals(expected, in); + assertEquals(expected, gteLte); + assertEquals(expected, between); + assertEquals(expected, gteLimited); + } + + @Test + public void shouldDeriveToEmptyClause() { + List 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")); + } + } } diff --git a/src/integration/java/org/springframework/data/couchbase/repository/CustomUserRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/CustomUserRepository.java index cad54dc4..cd334a21 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/CustomUserRepository.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/CustomUserRepository.java @@ -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 { @@ -31,7 +34,42 @@ public interface CustomUserRepository extends CouchbaseRepository @View(designDocument = "userCustom", viewName = "customCountView") long count(); - @View - Iterable findAllSomething(); + @View(viewName = "allSomething") + Iterable findIncorrectExplicitView(); + @View(viewName = "customFindAllView") + Iterable findRandomMethodName(); + + @View(viewName = "customFindByNameView") + long countByUsernameGreaterThanEqualAndUsernameLessThan(String lowBound, String highBound); + + @View(viewName = "customFindByNameView") + User findByUsernameIs(String lowKey); + + @View(viewName = "customFindByNameView") + List findAllByUsernameIn(List keys); + + @View(viewName = "customFindByNameView") + List findByUsernameGreaterThanEqualAndUsernameLessThanEqual(String lowKey, String highKey); + + @View(viewName = "customFindByNameView") + List findByUsernameBetween(String lowKey, String highKey); + + @View(viewName = "customFindByNameView") + List findTop3ByUsernameGreaterThanEqual(String lowKey); + + @View(viewName = "customFindAllView") + List findAllByUsername(); + + @View(viewName = "customFindAllView") + List findAllByUsernameEqualAndUserblablaIs(String s, String blabla); + + @View + List findByIncorrectView(); + + @View + long countCustomFindAllView(); + + @View + long countCustomFindInvalid(); } diff --git a/src/integration/java/org/springframework/data/couchbase/repository/User.java b/src/integration/java/org/springframework/data/couchbase/repository/User.java index c5e1c257..adbfef81 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/User.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/User.java @@ -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(); + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/view/View.java b/src/main/java/org/springframework/data/couchbase/core/view/View.java index 1767a523..87f12ea8 100644 --- a/src/main/java/org/springframework/data/couchbase/core/view/View.java +++ b/src/main/java/org/springframework/data/couchbase/core/view/View.java @@ -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. - *

- * 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. - *

- * 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 */ diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java index cb645e99..0661e7d6 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseQueryMethod.java @@ -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()); } /** diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ViewBasedCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ViewBasedCouchbaseQuery.java index df1cd860..67d60de4 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ViewBasedCouchbaseQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ViewBasedCouchbaseQuery.java @@ -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 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"); - } - } - } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ViewQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/ViewQueryCreator.java new file mode 100644 index 00000000..23b737a7 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ViewQueryCreator.java @@ -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}. + *

+ * 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). + *
+ * Here are the {@link Part.Type} supported: + *

    + *
  • GREATER_THAN_EQUAL: uses {@link ViewQuery#startKey(String) startkey}
  • + *
  • LESS_THAN_EQUAL: uses {@link ViewQuery#endKey(String) endkey} {@link ViewQuery#inclusiveEnd() inclusive}
  • + *
  • LESS_THAN / BEFORE: uses {@link ViewQuery#endKey(String) endkey}
  • + *
  • BETWEEN: uses {@link ViewQuery#startKey(String) startkey}, + * {@link ViewQuery#endKey(String) endkey(exclusive)}
  • + *
  • STARTING_WITH: (only with String key). uses {@link ViewQuery#startKey(String) startkey}, + * {@link ViewQuery#endKey(String) endkey(exclusive)}. Will append special unicode char \uefff.
  • + *
  • SIMPLE_PROPERTY: (aka "Is", "Equals"). This one can have no argument if used alone + * (eg. "findAllByUsername"), otherwise uses {@link ViewQuery#key(String) key}
  • + *
  • IN: uses {@link ViewQuery#keys(JsonArray) keys} (provide a collection or array)
  • + *
+ *

+ * 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 { + + 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 properties = new HashSet(); + 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 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 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 arrayContent = new ArrayList((Collection) next); + query.startKey(JsonArray.from(arrayContent)); + } else if (next.getClass().isArray()) { + List 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 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 arrayContent = new ArrayList((Collection) next); + query.endKey(JsonArray.from(arrayContent)); + } else if (next.getClass().isArray()) { + List 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 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 arrayContent = new ArrayList((Collection) next); + query.key(JsonArray.from(arrayContent)); + } else if (next.getClass().isArray()) { + List 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 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 iterator) { + if (!iterator.hasNext()) { + throw new IllegalArgumentException("Not enough parameters for in"); + } + Object next = iterator.next(); + + List values; + if (next instanceof Collection) { + values = new ArrayList((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 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 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; + } +}