From c29727595ce64ac90eb74a807bd0f5cd78ff5537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 7 Jul 2015 19:41:27 +0200 Subject: [PATCH] DATACOUCH-137 - queryDerivation for N1QL annotated methods. Abstracted the N1QL-based variants in AbstractN1qlBasedQuery. This implementation doesn't recognize QueryParams or QueryPlan anymore in the method parameters so that the method signatures are not store-dependent. Added query derivation from PartTree to N1QL query, using N1qlQueryCreator. Unit tested the mapping between a Part.Type and the corresponding N1QL Expression. --- .../SimpleCouchbaseRepositoryTests.java | 13 + .../couchbase/repository/UserRepository.java | 5 + .../config/CouchbaseNamespaceHandler.java | 5 +- .../mapping/CouchbasePersistentProperty.java | 14 + .../query/AbstractN1qlBasedQuery.java | 48 +- .../query/CouchbaseQueryMethod.java | 4 +- .../repository/query/N1qlQueryCreator.java | 301 +++++++++++ .../query/PartTreeN1qlBasedQuery.java | 73 +++ .../query/StringN1qlBasedQuery.java | 14 +- .../support/CouchbaseRepositoryFactory.java | 7 +- .../query/AbstractN1qlBasedQueryTest.java | 83 +-- .../query/N1qlQueryCreatorTest.java | 489 ++++++++++++++++++ 12 files changed, 947 insertions(+), 109 deletions(-) create mode 100644 src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java create mode 100644 src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java create mode 100644 src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTest.java diff --git a/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java b/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java index e547bdf6..7349eebb 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTests.java @@ -18,10 +18,13 @@ package org.springframework.data.couchbase.repository; import static org.junit.Assert.*; +import java.util.Arrays; + import com.couchbase.client.java.Bucket; import com.couchbase.client.java.view.Stale; import com.couchbase.client.java.view.ViewQuery; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -101,6 +104,8 @@ public class SimpleCouchbaseRepositoryTests { } @Test + @Ignore("View based query with copy of params from a ViewQuery in the method parameter not implemented") + //TODO re-enable test once ViewQuery parameters other than designDoc/viewName can be copied public void shouldFindCustom() { Iterable users = repository.customViewQuery(ViewQuery.from("", "").limit(2).stale(Stale.FALSE)); int size = 0; @@ -133,4 +138,12 @@ public class SimpleCouchbaseRepositoryTests { } } + @Test + public void shouldFindFromDeriveQueryWithRegexpAndIn() { + User user = repository.findByUsernameRegexAndUsernameIn("uname-[123]", Arrays.asList("uname-2", "uname-4")); + assertNotNull(user); + assertEquals("testuser-2", user.getKey()); + assertEquals("uname-2", user.getUsername()); + } + } diff --git a/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java b/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java index f2a39a39..48f2703c 100644 --- a/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java +++ b/src/integration/java/org/springframework/data/couchbase/repository/UserRepository.java @@ -16,6 +16,8 @@ package org.springframework.data.couchbase.repository; +import java.util.List; + import com.couchbase.client.java.view.ViewQuery; import org.springframework.data.couchbase.core.view.N1QL; @@ -35,4 +37,7 @@ public interface UserRepository extends CouchbaseRepository { @N1QL("SELECT * FROM $BUCKET$ WHERE username = $1") User findByUsernameBadSelect(String username); + @N1QL + User findByUsernameRegexAndUsernameIn(String regex, List sample); + } diff --git a/src/main/java/org/springframework/data/couchbase/config/CouchbaseNamespaceHandler.java b/src/main/java/org/springframework/data/couchbase/config/CouchbaseNamespaceHandler.java index bebed47d..a83e5371 100644 --- a/src/main/java/org/springframework/data/couchbase/config/CouchbaseNamespaceHandler.java +++ b/src/main/java/org/springframework/data/couchbase/config/CouchbaseNamespaceHandler.java @@ -18,6 +18,8 @@ package org.springframework.data.couchbase.config; import org.springframework.beans.factory.xml.NamespaceHandler; import org.springframework.beans.factory.xml.NamespaceHandlerSupport; +import org.springframework.data.couchbase.repository.config.CouchbaseRepositoryConfigurationExtension; +import org.springframework.data.repository.config.RepositoryBeanDefinitionParser; /** * {@link NamespaceHandler} for Couchbase configuration. @@ -33,7 +35,8 @@ public class CouchbaseNamespaceHandler extends NamespaceHandlerSupport { * Register bean definition parsers in the namespace handler. */ public final void init() { - //TODO repositories (CouchbaseRepositoryConfigurationExtension and RepositoryBeanDefinitionParser) + CouchbaseRepositoryConfigurationExtension extension = new CouchbaseRepositoryConfigurationExtension(); + registerBeanDefinitionParser("repositories", new RepositoryBeanDefinitionParser(extension)); registerBeanDefinitionParser("env", new CouchbaseEnvironmentParser()); registerBeanDefinitionParser("cluster", new CouchbaseClusterParser()); registerBeanDefinitionParser("bucket", new CouchbaseBucketParser()); diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentProperty.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentProperty.java index dd2cce4d..04c29785 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentProperty.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentProperty.java @@ -16,7 +16,9 @@ package org.springframework.data.couchbase.core.mapping; +import org.springframework.core.convert.converter.Converter; import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.context.PersistentPropertyPath; /** * Represents a property part of an entity that needs to be persisted. @@ -32,4 +34,16 @@ public interface CouchbasePersistentProperty extends PersistentProperty FIELD_NAME = new Converter() { + @Override + public String convert(CouchbasePersistentProperty source) { + return source.getFieldName(); + } + }; + + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java index 86e1d990..d092c1a7 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java @@ -16,13 +16,10 @@ package org.springframework.data.couchbase.repository.query; -import java.util.Iterator; import java.util.List; import com.couchbase.client.java.document.json.JsonArray; import com.couchbase.client.java.query.Query; -import com.couchbase.client.java.query.QueryParams; -import com.couchbase.client.java.query.QueryPlan; import com.couchbase.client.java.query.Statement; import org.springframework.data.couchbase.core.CouchbaseOperations; @@ -38,7 +35,7 @@ import org.springframework.data.util.StreamUtils; */ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { - private final CouchbaseQueryMethod queryMethod; + protected final CouchbaseQueryMethod queryMethod; private final CouchbaseOperations couchbaseOperations; protected AbstractN1qlBasedQuery(CouchbaseQueryMethod queryMethod, CouchbaseOperations couchbaseOperations) { @@ -46,45 +43,28 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { this.couchbaseOperations = couchbaseOperations; } - protected abstract Statement getStatement(); + protected abstract Statement getStatement(ParameterAccessor accessor); + + protected abstract JsonArray getPlaceholderValues(ParameterAccessor accessor); @Override public Object execute(Object[] parameters) { - Statement statement = getStatement(); + ParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); + Statement statement = getStatement(accessor); + JsonArray queryPlaceholderValues = getPlaceholderValues(accessor); - ParameterAccessor parameterAccessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); - Query query = buildQuery(statement, parameterAccessor.iterator()); + Query query = buildQuery(statement, queryPlaceholderValues); return executeDependingOnType(query, queryMethod, queryMethod.isPageQuery(), queryMethod.isModifyingQuery(), queryMethod.isSliceQuery()); } - protected static Query buildQuery(Statement statement, Iterator paramIterator) { - JsonArray queryValues = JsonArray.create(); - QueryParams queryParams = null; - QueryPlan preparedPayload = null; - - while (paramIterator.hasNext()) { - Object next = paramIterator.next(); - if (next instanceof QueryParams) { - queryParams = (QueryParams) next; - } - else if (next instanceof QueryPlan) { - preparedPayload = (QueryPlan) next; - } - else { - queryValues.add(next); - } - } - + protected static Query buildQuery(Statement statement, JsonArray queryPlaceholderValues) { Query query; - if (preparedPayload != null) { - query = Query.prepared(preparedPayload, queryValues.isEmpty() ? null : queryValues, queryParams); - } - else if (!queryValues.isEmpty()) { - query = Query.parametrized(statement, queryValues, queryParams); + if (!queryPlaceholderValues.isEmpty()) { + query = Query.parametrized(statement, queryPlaceholderValues); } else { - query = Query.simple(statement, queryParams); + query = Query.simple(statement); } return query; @@ -123,4 +103,8 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { public CouchbaseQueryMethod getQueryMethod() { return this.queryMethod; } + + protected CouchbaseOperations getCouchbaseOperations() { + return this.couchbaseOperations; + } } 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 d9e167dc..ffb06d5f 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 @@ -16,6 +16,8 @@ package org.springframework.data.couchbase.repository.query; +import java.lang.reflect.Method; + import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; @@ -26,8 +28,6 @@ import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryMethod; import org.springframework.util.StringUtils; -import java.lang.reflect.Method; - /** * Represents a query method with couchbase extensions, allowing to discover * if View-based query or N1QL-based query must be used. diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java new file mode 100644 index 00000000..684e8163 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java @@ -0,0 +1,301 @@ +/* + * 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 static com.couchbase.client.java.query.dsl.Expression.s; +import static com.couchbase.client.java.query.dsl.Expression.x; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import com.couchbase.client.java.document.json.JsonArray; +import com.couchbase.client.java.query.dsl.Expression; +import com.couchbase.client.java.query.dsl.functions.StringFunctions; +import com.couchbase.client.java.query.dsl.path.LimitPath; +import com.couchbase.client.java.query.dsl.path.OrderByPath; +import com.couchbase.client.java.query.dsl.path.WherePath; + +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.context.PersistentPropertyPath; +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; + +/** + * This {@link AbstractQueryCreator Query Creator} is responsible for parsing a {@link PartTree} (representing + * a method name) into the WHERE clause of a N1QL query. + *

+ * In the following, "field" represents the path in JSON deduced from the part of the method name. "a" and "b" represent + * the values of next consumed method parameters. "array" represent a {@link JsonArray} constructed from the next method + * parameter value (if a collection or array, contained values are used to fill the array, otherwise it's a single item + * array). + *
+ * Here are the {@link Part.Type} supported (field: + *

    + *
  • BETWEEN: field BETWEEN a AND b
  • + *
  • IS_NOT_NULL: field IS NOT NULL
  • + *
  • IS_NULL: field IS NULL
  • + *
  • NEGATING_SIMPLE_PROPERTY: - field != a
  • + *
  • SIMPLE_PROPERTY: - field = a
  • + *
  • LESS_THAN: field < a
  • + *
  • LESS_THAN_EQUAL: field <= a
  • + *
  • GREATER_THAN_EQUAL: field >= a
  • + *
  • GREATER_THAN: field > a
  • + *
  • BEFORE: field < a
  • + *
  • AFTER: field > a
  • + *
  • NOT_LIKE: field NOT LIKE "a" - a should be a String containing % and _ (matching n and 1 characters)
  • + *
  • LIKE: field LIKE "a" - a should be a String containing % and _ (matching n and 1 characters)
  • + *
  • STARTING_WITH: field LIKE "a%" - a should be a String prefix
  • + *
  • ENDING_WITH: field LIKE "%a" - a should be a String suffix
  • + *
  • NOT_CONTAINING: field NOT LIKE "%a%" - a should be a String
  • + *
  • CONTAINING: field LIKE "%a%" - a should be a String
  • + *
  • NOT_IN: field NOT IN array - note that the next parameter value (or its children if a collection/array) + * should be compatible for storage in a {@link JsonArray})
  • + *
  • IN: field IN array - note that the next parameter value (or its children if a collection/array) should + * be compatible for storage in a {@link JsonArray})
  • + *
  • TRUE: field = TRUE
  • + *
  • FALSE: field = FALSE
  • + *
  • REGEX: REGEXP_LIKE(field, "a") - note that the ignoreCase is ignored here, a is a regular expression + * in String form
  • + *
  • EXISTS: field IS NOT MISSING - used to verify that the JSON contains this attribute
  • + *
+ *
+ * The following are not supported and will throw an {@link IllegalArgumentException} if encountered: + *
    + *
  • NEAR, WITHIN: geospatial is not supported in N1QL as of now
  • + *
+ *

+ * + * @author Simon Baslé + */ +public class N1qlQueryCreator extends AbstractQueryCreator { + + private final ParameterAccessor accessor; + private final WherePath selectFrom; + private final MappingContext, CouchbasePersistentProperty> context; + + public N1qlQueryCreator(PartTree tree, ParameterAccessor parameters, WherePath selectFrom, + CouchbaseConverter converter) { + super(tree, parameters); + this.accessor = parameters; + this.selectFrom = selectFrom; + this.context = converter.getMappingContext(); + } + + @Override + protected Expression create(Part part, Iterator iterator) { + PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); + return prepareExpression(part, path, iterator); + } + + @Override + protected Expression and(Part part, Expression base, Iterator iterator) { + if (base == null) { + return create(part, iterator); + } + + return base.and(create(part, iterator)); + } + + @Override + protected Expression or(Expression base, Expression criteria) { + return base.or(criteria); + } + + @Override + protected LimitPath complete(Expression criteria, Sort sort) { + OrderByPath selectFromWhere = selectFrom.where(criteria); + + if (sort != null) { + List cbSortList = new ArrayList(); + for (Sort.Order order : sort) { + if (order.isAscending()) { + cbSortList.add(com.couchbase.client.java.query.dsl.Sort.asc(order.getProperty())); + } else { + cbSortList.add(com.couchbase.client.java.query.dsl.Sort.desc(order.getProperty())); + } + } + com.couchbase.client.java.query.dsl.Sort[] cbSorts = + cbSortList.toArray(new com.couchbase.client.java.query.dsl.Sort[cbSortList.size()]); + return selectFromWhere.orderBy(cbSorts); + } + + return selectFrom.where(criteria); + } + + protected static Expression prepareExpression(Part part, PersistentPropertyPath path, + Iterator parameterValues) { + //FIXME use conversions for the parameters and types + //get the whole doted path with fieldNames instead of potentially wrong propNames + String fieldNamePath = path.toDotPath(CouchbasePersistentProperty.FIELD_NAME); + + //deal with ignore case + boolean ignoreCase = false; + Class leafType = path.getLeafProperty().getType(); + boolean isString = leafType == String.class; + if (part.shouldIgnoreCase() == Part.IgnoreCaseType.WHEN_POSSIBLE) { + ignoreCase = isString; + } else if (part.shouldIgnoreCase() == Part.IgnoreCaseType.ALWAYS) { + if (!isString) { + throw new IllegalArgumentException(String.format("Part %s must be of type String but was %s", fieldNamePath, leafType)); + } + ignoreCase = true; + } + + return createExpression(part.getType(), fieldNamePath, ignoreCase, parameterValues); + } + + protected static Expression createExpression(Part.Type partType, String fieldNamePath, boolean ignoreCase, Iterator parameterValues) { + //create the left hand side of the expression, taking ignoreCase into account + Expression left = ignoreCase ? StringFunctions.lower(x(fieldNamePath)) : x(fieldNamePath); + + switch (partType) { + case BETWEEN: + return left.between(leftAndRight(parameterValues, ignoreCase)); + case IS_NOT_NULL: + return left.isNotNull(); + case IS_NULL: + return left.isNull(); + case NEGATING_SIMPLE_PROPERTY: + return left.ne(right(parameterValues, ignoreCase)); + case SIMPLE_PROPERTY: + return left.eq(right(parameterValues, ignoreCase)); + case BEFORE: + case LESS_THAN: + return left.lt(right(parameterValues, ignoreCase)); + case LESS_THAN_EQUAL: + return left.lte(right(parameterValues, ignoreCase)); + case GREATER_THAN_EQUAL: + return left.gte(right(parameterValues, ignoreCase)); + case AFTER: + case GREATER_THAN: + return left.gt(right(parameterValues, ignoreCase)); + case NOT_LIKE: + return left.notLike(right(parameterValues, ignoreCase)); + case LIKE: + return left.like(right(parameterValues, ignoreCase)); + case STARTING_WITH: + return left.like(like(parameterValues, ignoreCase, false, true)); + case ENDING_WITH: + return left.like(like(parameterValues, ignoreCase, true, false)); + case NOT_CONTAINING: + return left.notLike(like(parameterValues, ignoreCase, true, true)); + case CONTAINING: + return left.like(like(parameterValues, ignoreCase, true, true)); + case NOT_IN: + return left.notIn(rightArray(parameterValues)); + case IN: + return left.in(rightArray(parameterValues)); + case TRUE: + return left.eq(true); + case FALSE: + return left.eq(false); + case REGEX: + return regexp(fieldNamePath, parameterValues); + case EXISTS: + return left.isNotMissing(); + case WITHIN: + case NEAR: + default: + throw new IllegalArgumentException("Unsupported keyword"); + } + } + + protected static Expression regexp(String left, Iterator parameterValues) { + //TODO migrate to using the Functions util class when 2.0-dp2 / 2.0 GA + Object next = parameterValues.next(); + String pattern; + if (next == null) { + pattern = ""; + } else { + pattern = String.valueOf(next); + } + return x("REGEXP_LIKE(" + left + ", \"" + pattern + "\")"); + } + + protected static Expression leftAndRight(Iterator parameterValues, boolean ignoreCase) { + return right(parameterValues, ignoreCase).and(right(parameterValues, ignoreCase)); + } + + protected static Expression like(Iterator parameterValues, boolean ignoreCase, + boolean anyPrefix, boolean anySuffix) { + Object next = parameterValues.next(); + if (next == null) { + return Expression.NULL(); + } + + Expression converted; + if (next instanceof String) { + String pattern = (String) next; + if (anyPrefix) { + pattern = "%" + pattern; + } + if (anySuffix) { + pattern = pattern + "%"; + } + converted = s(pattern); + + } else { + converted = x(String.valueOf(next)); + } + + if (ignoreCase) { + return StringFunctions.lower(converted); + } + return converted; + } + + protected static Expression right(Iterator parameterValues, boolean ignoreCase) { + Object next = parameterValues.next(); + if (next == null) { + return Expression.NULL(); + } + + Expression converted; + if (next instanceof String) { + converted = s((String) next); + } else { + converted = x(String.valueOf(next)); + } + + if (ignoreCase) { + return StringFunctions.lower(converted); + } + return converted; + } + + protected static JsonArray rightArray(Iterator parameterValues) { + Object next = parameterValues.next(); + + Object[] values; + if (next instanceof Collection) { + values = ((Collection) next).toArray(); + } else if (next.getClass().isArray()) { + values = (Object[]) next; + } else { + values = new Object[] {next}; + } + return JsonArray.from(values); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java new file mode 100644 index 00000000..fd378677 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java @@ -0,0 +1,73 @@ +/* + * 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 static com.couchbase.client.java.query.Select.select; +import static com.couchbase.client.java.query.dsl.Expression.i; +import static com.couchbase.client.java.query.dsl.Expression.path; +import static com.couchbase.client.java.query.dsl.functions.AggregateFunctions.count; +import static com.couchbase.client.java.query.dsl.functions.MetaFunctions.meta; + +import com.couchbase.client.java.document.json.JsonArray; +import com.couchbase.client.java.query.Statement; +import com.couchbase.client.java.query.dsl.Expression; +import com.couchbase.client.java.query.dsl.path.FromPath; +import com.couchbase.client.java.query.dsl.path.LimitPath; +import com.couchbase.client.java.query.dsl.path.WherePath; + +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.parser.PartTree; + +public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery { + + private final PartTree partTree; + + public PartTreeN1qlBasedQuery(CouchbaseQueryMethod queryMethod, CouchbaseOperations couchbaseOperations) { + super(queryMethod, couchbaseOperations); + this.partTree = new PartTree(queryMethod.getName(), queryMethod.getEntityInformation().getJavaType()); + } + + @Override + protected JsonArray getPlaceholderValues(ParameterAccessor accessor) { + return JsonArray.empty(); + } + + @Override + protected Statement getStatement(ParameterAccessor accessor) { + Expression bucket = i(getCouchbaseOperations().getCouchbaseBucket().name()); + Expression metaId = path(meta(bucket), "id").as(CouchbaseOperations.SELECT_ID); + Expression metaCas = path(meta(bucket), "cas").as(CouchbaseOperations.SELECT_CAS); + + FromPath select; + if (partTree.isCountProjection()) { + select = select(count("*")); + } else { + select = select(metaId, metaCas, path(bucket, "*")); + } + WherePath selectFrom = select.from(bucket); + + N1qlQueryCreator queryCreator = new N1qlQueryCreator(partTree, accessor, selectFrom, getCouchbaseOperations().getConverter()); + LimitPath selectFromWhereOrderBy = queryCreator.createQuery(); + + if (partTree.isLimiting()) { + return selectFromWhereOrderBy.limit(partTree.getMaxResults()); + } else { + return selectFromWhereOrderBy; + } + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java index 2bdb49c2..7516067c 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java @@ -16,10 +16,12 @@ package org.springframework.data.couchbase.repository.query; +import com.couchbase.client.java.document.json.JsonArray; import com.couchbase.client.java.query.Query; import com.couchbase.client.java.query.Statement; import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; /** @@ -63,7 +65,17 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery { } @Override - public Statement getStatement() { + protected JsonArray getPlaceholderValues(ParameterAccessor accessor) { + JsonArray values = JsonArray.create(); + for (Object value : accessor) { + values.add(value); + } + return values; + } + + @Override + public Statement getStatement(ParameterAccessor accessor) { return this.statement; } + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java index 4c69668b..29c7d8b1 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseRepositoryFactory.java @@ -24,6 +24,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod; +import org.springframework.data.couchbase.repository.query.PartTreeN1qlBasedQuery; import org.springframework.data.couchbase.repository.query.StringN1qlBasedQuery; import org.springframework.data.couchbase.repository.query.ViewBasedCouchbaseQuery; import org.springframework.data.mapping.context.MappingContext; @@ -32,6 +33,7 @@ import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.util.Assert; @@ -121,7 +123,7 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { } @Override - protected QueryLookupStrategy getQueryLookupStrategy(QueryLookupStrategy.Key key) { + protected QueryLookupStrategy getQueryLookupStrategy(QueryLookupStrategy.Key key, EvaluationContextProvider contextProvider) { return new CouchbaseQueryLookupStrategy(); } @@ -141,8 +143,7 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { String namedQuery = namedQueries.getQuery(namedQueryName); return new StringN1qlBasedQuery(namedQuery, queryMethod, couchbaseOperations); } else { - //FIXME return new PartBasedN1qlQuery(queryMethod, couchbaseOperations, namedQueries); - return new StringN1qlBasedQuery("SELECT 1", queryMethod, couchbaseOperations); + return new PartTreeN1qlBasedQuery(queryMethod, couchbaseOperations); } } return new ViewBasedCouchbaseQuery(queryMethod, couchbaseOperations); diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java b/src/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java index 38dc2bca..bc3b7032 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQueryTest.java @@ -4,68 +4,28 @@ import static com.couchbase.client.java.query.Select.select; import static org.junit.Assert.*; import static org.mockito.Mockito.*; -import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.concurrent.TimeUnit; import com.couchbase.client.java.document.json.JsonArray; import com.couchbase.client.java.document.json.JsonObject; import com.couchbase.client.java.query.ParametrizedQuery; -import com.couchbase.client.java.query.PreparedQuery; import com.couchbase.client.java.query.Query; -import com.couchbase.client.java.query.QueryParams; -import com.couchbase.client.java.query.QueryPlan; -import com.couchbase.client.java.query.Select; import com.couchbase.client.java.query.SimpleQuery; import com.couchbase.client.java.query.Statement; -import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; import org.mockito.Mockito; -import org.springframework.data.couchbase.UnitTestApplicationConfig; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; -import org.springframework.data.domain.Page; -import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.repository.core.RepositoryMetadata; -import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethod; -import org.springframework.data.util.ReflectionUtils; public class AbstractN1qlBasedQueryTest { - @Test - public void testQueryPlanShouldProducePreparedQuery() throws Exception { - Statement st = select("*"); - QueryPlan plan = new QueryPlan(JsonObject.create()); - List params = new ArrayList(2); - params.add("test"); - params.add(plan); - - Query query = AbstractN1qlBasedQuery.buildQuery(st, params.iterator()); - JsonObject queryObject = query.n1ql(); - assertTrue(query instanceof PreparedQuery); - assertEquals(plan, query.statement()); - assertNull(query.params()); - assertNotNull(queryObject.get("args")); - assertEquals(1, queryObject.getArray("args").size()); - assertEquals("test", queryObject.getArray("args").getString(0)); - - params.add(QueryParams.build()); - query = AbstractN1qlBasedQuery.buildQuery(st, params.iterator()); - assertNotNull(query.params()); - } - @Test public void testEmptyArgumentsShouldProduceSimpleQuery() throws Exception { Statement st = select("*"); - List params = Collections.emptyList(); - - Query query = AbstractN1qlBasedQuery.buildQuery(st, params.iterator()); + Query query = AbstractN1qlBasedQuery.buildQuery(st, JsonArray.empty()); JsonObject queryObject = query.n1ql(); + assertTrue(query instanceof SimpleQuery); assertEquals(st.toString(), query.statement().toString()); assertNull(query.params()); @@ -73,53 +33,36 @@ public class AbstractN1qlBasedQueryTest { } @Test - public void testOnlyQueryParamsShouldProduceSimpleQuery() { - Statement st = select("*"); - QueryParams queryParams = QueryParams.build().scanWait(1, TimeUnit.DAYS); - List params = new ArrayList(1); - params.add(queryParams); - - Query query = AbstractN1qlBasedQuery.buildQuery(st, params.iterator()); - JsonObject queryObject = query.n1ql(); - assertTrue(query instanceof SimpleQuery); - assertEquals(st.toString(), query.statement().toString()); - assertEquals(queryParams, query.params()); - assertFalse(queryObject.containsKey("args")); - } - - @Test - public void testSimpleArgumentsShouldProduceParametrizedQuery() throws Exception { + public void testSimpleArgumentShouldProduceParametrizedQuery() throws Exception { Statement st = select("*"); List params = new ArrayList(2); - params.add(123L); params.add("test"); - - Query query = AbstractN1qlBasedQuery.buildQuery(st, params.iterator()); + JsonArray placeholderValues = JsonArray.from(params); + Query query = AbstractN1qlBasedQuery.buildQuery(st, placeholderValues); JsonObject queryObject = query.n1ql(); + assertTrue(query instanceof ParametrizedQuery); assertEquals(st.toString(), query.statement().toString()); assertNull(query.params()); assertTrue(queryObject.containsKey("args")); JsonArray args = queryObject.getArray("args"); - assertEquals(2, args.size()); - assertEquals(123L, args.get(0)); - assertEquals("test", args.get(1)); + assertEquals(1, args.size()); + assertEquals("test", args.get(0)); } @Test - public void testSimpleArgumentsAndQueryParamsShouldProduceParametrizedQuery() throws Exception { + public void testMultipleArgumentsShouldProduceParametrizedQuery() throws Exception { Statement st = select("*"); - QueryParams queryParams = QueryParams.build().withContextId("toto"); List params = new ArrayList(2); params.add(123L); - params.add(queryParams); params.add("test"); - - Query query = AbstractN1qlBasedQuery.buildQuery(st, params.iterator()); + JsonArray placeholderValues = JsonArray.from(params); + Query query = AbstractN1qlBasedQuery.buildQuery(st, placeholderValues); JsonObject queryObject = query.n1ql(); + assertTrue(query instanceof ParametrizedQuery); assertEquals(st.toString(), query.statement().toString()); - assertEquals(queryParams, query.params()); + assertNull(query.params()); assertTrue(queryObject.containsKey("args")); JsonArray args = queryObject.getArray("args"); assertEquals(2, args.size()); diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTest.java b/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTest.java new file mode 100644 index 00000000..57a75c86 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTest.java @@ -0,0 +1,489 @@ +package org.springframework.data.couchbase.repository.query; + +import static org.junit.Assert.assertEquals; +import static org.springframework.data.couchbase.repository.query.N1qlQueryCreator.createExpression; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import com.couchbase.client.java.query.dsl.Expression; +import org.junit.Test; + +import org.springframework.data.repository.query.parser.Part; + +public class N1qlQueryCreatorTest { + + //==== The tests below check mapping between a Part.Type and the corresponding N1QL expression ==== + + @Test + public void testBETWEEN() throws Exception { + Part.Type keyword = Part.Type.BETWEEN; + Iterator values = Arrays.asList("a", "b", 1, 2, "c", "d").iterator(); + String expected = "doc.field BETWEEN \"a\" AND \"b\""; + String expectedNum = "doc.field BETWEEN 1 AND 2"; + String expectedIgnoreCase = "LOWER(doc.field) BETWEEN LOWER(\"c\") AND LOWER(\"d\")"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testIS_NOT_NULL() throws Exception { + Part.Type keyword = Part.Type.IS_NOT_NULL; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field IS NOT NULL"; + String expectedNum = "doc.field IS NOT NULL"; + String expectedIgnoreCase = "LOWER(doc.field) IS NOT NULL"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testIS_NULL() throws Exception { + Part.Type keyword = Part.Type.IS_NULL; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field IS NULL"; + String expectedNum = "doc.field IS NULL"; + String expectedIgnoreCase = "LOWER(doc.field) IS NULL"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testLESS_THAN() throws Exception { + Part.Type keyword = Part.Type.LESS_THAN; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field < \"a\""; + String expectedNum = "doc.field < 1"; + String expectedIgnoreCase = "LOWER(doc.field) < LOWER(\"b\")"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testLESS_THAN_EQUAL() throws Exception { + Part.Type keyword = Part.Type.LESS_THAN_EQUAL; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field <= \"a\""; + String expectedNum = "doc.field <= 1"; + String expectedIgnoreCase = "LOWER(doc.field) <= LOWER(\"b\")"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testGREATER_THAN() throws Exception { + Part.Type keyword = Part.Type.GREATER_THAN; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field > \"a\""; + String expectedNum = "doc.field > 1"; + String expectedIgnoreCase = "LOWER(doc.field) > LOWER(\"b\")"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testGREATER_THAN_EQUAL() throws Exception { + Part.Type keyword = Part.Type.GREATER_THAN_EQUAL; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field >= \"a\""; + String expectedNum = "doc.field >= 1"; + String expectedIgnoreCase = "LOWER(doc.field) >= LOWER(\"b\")"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testBEFORE() throws Exception { + Part.Type keyword = Part.Type.BEFORE; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field < \"a\""; + String expectedNum = "doc.field < 1"; + String expectedIgnoreCase = "LOWER(doc.field) < LOWER(\"b\")"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testAFTER() throws Exception { + Part.Type keyword = Part.Type.AFTER; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field > \"a\""; + String expectedNum = "doc.field > 1"; + String expectedIgnoreCase = "LOWER(doc.field) > LOWER(\"b\")"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testNOT_LIKE() throws Exception { + Part.Type keyword = Part.Type.NOT_LIKE; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field NOT LIKE \"a\""; + String expectedNum = "doc.field NOT LIKE 1"; + String expectedIgnoreCase = "LOWER(doc.field) NOT LIKE LOWER(\"b\")"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testLIKE() throws Exception { + Part.Type keyword = Part.Type.LIKE; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field LIKE \"a\""; + String expectedNum = "doc.field LIKE 1"; + String expectedIgnoreCase = "LOWER(doc.field) LIKE LOWER(\"b\")"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testSTARTING_WITH() throws Exception { + Part.Type keyword = Part.Type.STARTING_WITH; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field LIKE \"a%\""; + String expectedNum = "doc.field LIKE 1"; + String expectedIgnoreCase = "LOWER(doc.field) LIKE LOWER(\"b%\")"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testENDING_WITH() throws Exception { + Part.Type keyword = Part.Type.ENDING_WITH; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field LIKE \"%a\""; + String expectedNum = "doc.field LIKE 1"; + String expectedIgnoreCase = "LOWER(doc.field) LIKE LOWER(\"%b\")"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testNOT_CONTAINING() throws Exception { + Part.Type keyword = Part.Type.NOT_CONTAINING; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field NOT LIKE \"%a%\""; + String expectedNum = "doc.field NOT LIKE 1"; + String expectedIgnoreCase = "LOWER(doc.field) NOT LIKE LOWER(\"%b%\")"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testCONTAINING() throws Exception { + Part.Type keyword = Part.Type.CONTAINING; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field LIKE \"%a%\""; + String expectedNum = "doc.field LIKE 1"; + String expectedIgnoreCase = "LOWER(doc.field) LIKE LOWER(\"%b%\")"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testNOT_IN() throws Exception { + Part.Type keyword = Part.Type.NOT_IN; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field NOT IN [\"a\"]"; + String expectedNum = "doc.field NOT IN [1]"; + String expectedIgnoreCase = "LOWER(doc.field) NOT IN [\"b\"]"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testNOTINwithCollection() throws Exception { + Part.Type keyword = Part.Type.NOT_IN; + List val1 = Arrays.asList("av1", "av2"); + List val2 = Arrays.asList("bv1", "bv2"); + Iterator values = Arrays.asList(val1, val2).iterator(); + String expected = "doc.field NOT IN [\"av1\",\"av2\"]"; + String expectedIgnoreCase = "LOWER(doc.field) NOT IN [\"bv1\",\"bv2\"]"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testNOTINwithArray() throws Exception { + Part.Type keyword = Part.Type.NOT_IN; + String[] val1 = {"av1", "av2"}; + String[] val2 = {"bv1", "bv2"}; + Iterator values = Arrays.asList(val1, val2).iterator(); + String expected = "doc.field NOT IN [\"av1\",\"av2\"]"; + String expectedIgnoreCase = "LOWER(doc.field) NOT IN [\"bv1\",\"bv2\"]"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testIN() throws Exception { + Part.Type keyword = Part.Type.IN; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field IN [\"a\"]"; + String expectedNum = "doc.field IN [1]"; + String expectedIgnoreCase = "LOWER(doc.field) IN [\"b\"]"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testINwithCollection() throws Exception { + Part.Type keyword = Part.Type.IN; + List val1 = Arrays.asList("av1", "av2"); + List val2 = Arrays.asList("bv1", "bv2"); + Iterator values = Arrays.asList(val1, val2).iterator(); + String expected = "doc.field IN [\"av1\",\"av2\"]"; + String expectedIgnoreCase = "LOWER(doc.field) IN [\"bv1\",\"bv2\"]"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testINwithArray() throws Exception { + Part.Type keyword = Part.Type.IN; + String[] val1 = {"av1", "av2"}; + String[] val2 = {"bv1", "bv2"}; + Iterator values = Arrays.asList(val1, val2).iterator(); + String expected = "doc.field IN [\"av1\",\"av2\"]"; + String expectedIgnoreCase = "LOWER(doc.field) IN [\"bv1\",\"bv2\"]"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test(expected = IllegalArgumentException.class) + public void testNEAR() throws Exception { + Part.Type keyword = Part.Type.NEAR; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + + Expression exp = createExpression(keyword, "doc.field", true, values); + } + + @Test(expected = IllegalArgumentException.class) + public void testWITHIN() throws Exception { + Part.Type keyword = Part.Type.WITHIN; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + + Expression exp = createExpression(keyword, "doc.field", false, values); + } + + @Test + public void testREGEX() throws Exception { + Part.Type keyword = Part.Type.REGEX; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "REGEXP_LIKE(doc.field, \"a\")"; + String expectedNum = "REGEXP_LIKE(doc.field, \"1\")"; + String expectedIgnoreCase = "REGEXP_LIKE(doc.field, \"b\")"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testEXISTS() throws Exception { + Part.Type keyword = Part.Type.EXISTS; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field IS NOT MISSING"; + String expectedNum = "doc.field IS NOT MISSING"; + String expectedIgnoreCase = "LOWER(doc.field) IS NOT MISSING"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testTRUE() throws Exception { + Part.Type keyword = Part.Type.TRUE; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field = TRUE"; + String expectedNum = "doc.field = TRUE"; + String expectedIgnoreCase = "LOWER(doc.field) = TRUE"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testFALSE() throws Exception { + Part.Type keyword = Part.Type.FALSE; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field = FALSE"; + String expectedNum = "doc.field = FALSE"; + String expectedIgnoreCase = "LOWER(doc.field) = FALSE"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testNEGATING_SIMPLE_PROPERTY() throws Exception { + Part.Type keyword = Part.Type.NEGATING_SIMPLE_PROPERTY; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field != \"a\""; + String expectedNum = "doc.field != 1"; + String expectedIgnoreCase = "LOWER(doc.field) != LOWER(\"b\")"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } + + @Test + public void testSIMPLE_PROPERTY() throws Exception { + Part.Type keyword = Part.Type.SIMPLE_PROPERTY; + Iterator values = Arrays.asList("a", 1, "b").iterator(); + String expected = "doc.field = \"a\""; + String expectedNum = "doc.field = 1"; + String expectedIgnoreCase = "LOWER(doc.field) = LOWER(\"b\")"; + + Expression exp = createExpression(keyword, "doc.field", false, values); + Expression expNum = createExpression(keyword, "doc.field", false, values); + Expression expIgnoreCase = createExpression(keyword, "doc.field", true, values); + + assertEquals(expected, exp.toString()); + assertEquals(expectedNum, expNum.toString()); + assertEquals(expectedIgnoreCase, expIgnoreCase.toString()); + } +} \ No newline at end of file