From 749c0a84e8ca4e9949ddcff74fa10b0b30829b93 Mon Sep 17 00:00:00 2001 From: mikereiche Date: Sun, 10 May 2020 01:50:13 -0700 Subject: [PATCH] DATACOUCH - 534 Add support for Query annotation --- ...ExecutableFindByQueryOperationSupport.java | 4 +- .../ReactiveFindByQueryOperationSupport.java | 88 +++----- .../data/couchbase/core/query/Query.java | 173 ++++++++++++++-- .../couchbase/core/query/QueryCriteria.java | 74 ++++--- .../core/query/QueryCriteriaDefinition.java | 17 ++ .../couchbase/core/query/StringQuery.java | 76 +++++++ .../couchbase/core/support/TemplateUtils.java | 6 +- .../query/CouchbaseRepositoryQuery.java | 14 +- .../repository/query/N1qlQueryCreator.java | 160 +++++++++------ .../query/N1qlRepositoryQueryExecutor.java | 45 ++++- .../ReactiveCouchbaseRepositoryQuery.java | 14 +- .../ReactiveN1qlRepositoryQueryExecutor.java | 50 ++++- .../query/ReactiveStringN1qlBasedQuery.java | 4 +- .../query/StringBasedN1qlQueryParser.java | 143 ++++++++++--- .../query/StringN1qlBasedQuery.java | 2 +- .../query/StringN1qlQueryCreator.java | 189 ++++++++++++++++++ .../support/CouchbaseRepositoryFactory.java | 18 +- .../ReactiveCouchbaseRepositoryFactory.java | 23 ++- .../core/query/QueryCriteriaTests.java | 23 ++- .../data/couchbase/domain/Config.java | 52 +++++ .../data/couchbase/domain/Person.java | 54 +++-- .../couchbase/domain/PersonRepository.java | 108 ++++++++++ .../data/couchbase/domain/UserRepository.java | 7 + .../query/N1qlQueryCreatorTests.java | 17 +- .../query/StringN1qlQueryCreatorTests.java | 182 +++++++++++++++++ .../util/ClusterAwareIntegrationTests.java | 26 ++- 26 files changed, 1315 insertions(+), 254 deletions(-) create mode 100644 src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java create mode 100644 src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java create mode 100644 src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java index fa4ce567..46859bd8 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableFindByQueryOperationSupport.java @@ -50,8 +50,8 @@ public class ExecutableFindByQueryOperationSupport implements ExecutableFindByQu this.template = template; this.domainType = domainType; this.query = query; - this.reactiveSupport = new ReactiveFindByQueryOperationSupport.ReactiveFindByQuerySupport<>(template.reactive(), - domainType, query, scanConsistency); + this.reactiveSupport = new ReactiveFindByQueryOperationSupport.ReactiveFindByQuerySupport( + template.reactive(), domainType, query, scanConsistency); this.scanConsistency = scanConsistency; } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java index 68029df0..94b6ff98 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByQueryOperationSupport.java @@ -15,16 +15,19 @@ */ package org.springframework.data.couchbase.core; +import org.springframework.data.couchbase.core.support.TemplateUtils; +import org.springframework.data.couchbase.core.query.Query; + import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.data.couchbase.core.query.Query; -import org.springframework.data.couchbase.core.query.QueryCriteria; - -import com.couchbase.client.java.query.QueryOptions; import com.couchbase.client.java.query.QueryScanConsistency; import com.couchbase.client.java.query.ReactiveQueryResult; +/** + * @author Michael Nitschinger + * @author Michael Reiche + */ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryOperation { private static final Query ALL_QUERY = new Query(); @@ -47,8 +50,8 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO private final Query query; private final QueryScanConsistency scanConsistency; - ReactiveFindByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, final Query query, - final QueryScanConsistency scanConsistency) { + ReactiveFindByQuerySupport(final ReactiveCouchbaseTemplate template, final Class domainType, + final Query query, final QueryScanConsistency scanConsistency) { this.template = template; this.domainType = domainType; this.query = query; @@ -79,20 +82,20 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO public Flux all() { return Flux.defer(() -> { String statement = assembleEntityQuery(false); - return template.getCouchbaseClientFactory().getCluster().reactive().query(statement, buildQueryOptions()) - .onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> { - String id = row.getString("__id"); - long cas = row.getLong("__cas"); - row.removeKey("__id"); - row.removeKey("__cas"); - return template.support().decodeEntity(id, row.toString(), cas, domainType); - }); + return template.getCouchbaseClientFactory().getCluster().reactive().query(statement, + query.buildQueryOptions(scanConsistency)).onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> { + String id = row.getString(TemplateUtils.SELECT_ID); + long cas = row.getLong(TemplateUtils.SELECT_CAS); + row.removeKey(TemplateUtils.SELECT_ID); + row.removeKey(TemplateUtils.SELECT_CAS); + return template.support().decodeEntity(id, row.toString(), cas, domainType); + }); }); } @@ -100,14 +103,15 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO public Mono count() { return Mono.defer(() -> { String statement = assembleEntityQuery(true); - return template.getCouchbaseClientFactory().getCluster().reactive().query(statement, buildQueryOptions()) - .onErrorMap(throwable -> { - if (throwable instanceof RuntimeException) { - return template.potentiallyConvertRuntimeException((RuntimeException) throwable); - } else { - return throwable; - } - }).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> row.getLong("__count")).next(); + return template.getCouchbaseClientFactory().getCluster().reactive().query(statement, + query.buildQueryOptions(scanConsistency)).onErrorMap(throwable -> { + if (throwable instanceof RuntimeException) { + return template.potentiallyConvertRuntimeException((RuntimeException) throwable); + } else { + return throwable; + } + }).flatMapMany(ReactiveQueryResult::rowsAsObject).map( + row -> row.getLong(TemplateUtils.SELECT_COUNT)).next(); }); } @@ -117,35 +121,9 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO } private String assembleEntityQuery(final boolean count) { - final String bucket = "`" + template.getBucketName() + "`"; - - final StringBuilder statement = new StringBuilder("SELECT "); - if (count) { - statement.append("count(*) as __count"); - } else { - statement.append("meta().id as __id, meta().cas as __cas, ").append(bucket).append(".*"); - } - - statement.append(" FROM ").append(bucket); - - String typeKey = template.getConverter().getTypeKey(); - String typeValue = template.support().getJavaNameForEntity(domainType); - query.addCriteria(QueryCriteria.where(typeKey).is(typeValue)); - - query.appendWhere(statement); - query.appendSort(statement); - query.appendSkipAndLimit(statement); - - return statement.toString(); + return query.toN1qlString(template, this.domainType, count); } - private QueryOptions buildQueryOptions() { - final QueryOptions options = QueryOptions.queryOptions(); - if (scanConsistency != null) { - options.scanConsistency(scanConsistency); - } - return options; - } } } diff --git a/src/main/java/org/springframework/data/couchbase/core/query/Query.java b/src/main/java/org/springframework/data/couchbase/core/query/Query.java index bb2ede37..1d57d85e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/Query.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/Query.java @@ -17,19 +17,38 @@ package org.springframework.data.couchbase.core.query; import java.util.ArrayList; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonObject; +import com.couchbase.client.java.json.JsonValue; +import com.couchbase.client.java.query.QueryOptions; +import com.couchbase.client.java.query.QueryScanConsistency; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.repository.query.StringBasedN1qlQueryParser; +import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.util.Assert; +/** + * @author Michael Nitschinger + * @author Michael Reiche + */ public class Query { private final List criteria = new ArrayList<>(); + private JsonValue parameters = JsonValue.ja(); private long skip; private int limit; private Sort sort = Sort.unsorted(); - public Query() {} + static private final Pattern WHERE_PATTERN = Pattern.compile("\\sWHERE\\s"); + + public Query() { + } public Query(final QueryCriteria criteriaDefinition) { addCriteria(criteriaDefinition); @@ -40,6 +59,34 @@ public class Query { return this; } + /** + * set the postional parameters on the query object + * There can only be named parameters or positional parameters - not both. + * + * @param parameters - the positional parameters + * @return - the query + */ + public Query setPositionalParameters(JsonArray parameters) { + this.parameters = parameters; + return this; + } + + /** + * set the named parameters on the query object + * There can only be named parameters or positional parameters - not both. + * + * @param parameters - the named parameters + * @return - the query + */ + public Query setNamedParameters(JsonObject parameters) { + this.parameters = parameters; + return this; + } + + JsonValue getParameters() { + return parameters; + } + /** * Set number of documents to skip before returning results. * @@ -110,33 +157,133 @@ public class Query { sb.append(" ORDER BY "); sort.stream().forEach(order -> { if (order.isIgnoreCase()) { - throw new IllegalArgumentException(String.format("Given sort contained an Order for %s with ignore case! " - + "Couchbase N1QL does not support sorting ignoring case currently!", order.getProperty())); + throw new IllegalArgumentException(String.format( + "Given sort contained an Order for %s with ignore case! " + + "Couchbase N1QL does not support sorting ignoring case currently!", + order.getProperty())); } sb.append(order.getProperty()).append(" ").append(order.isAscending() ? "ASC," : "DESC,"); }); sb.deleteCharAt(sb.length() - 1); } - public void appendWhere(final StringBuilder sb) { - sb.append(" WHERE "); - boolean first = true; - for (QueryCriteria c : criteria) { - if (first) { - first = false; - } else { - sb.append(" AND "); + public void appendWhere(final StringBuilder sb, int[] paramIndexPtr) { + if (!criteria.isEmpty()) { + appendWhereOrAnd(sb); + boolean first = true; + for (QueryCriteria c : criteria) { + if (first) { + first = false; + } else { + sb.append(" AND "); + } + sb.append(c.export(paramIndexPtr)); } - sb.append(c.export()); } } + public void appendCriteria(StringBuilder sb, QueryCriteria criteria) { + appendWhereOrAnd(sb); + sb.append(criteria.export()); + } + + public void appendWhereString(StringBuilder sb, String whereString) { + appendWhereOrAnd(sb); + sb.append(whereString); + } + + public void appendString(StringBuilder sb, String whereString) { + sb.append(whereString); + } + + private void appendWhereOrAnd(StringBuilder sb) { + String querySoFar = sb.toString().toUpperCase(); + Matcher whereMatcher = WHERE_PATTERN.matcher(querySoFar); + boolean alreadyWhere = false; + while (!alreadyWhere && whereMatcher.find()) { + if (notQuoted(whereMatcher.start(), whereMatcher.end(), querySoFar)) { + alreadyWhere = true; + } + } + if (alreadyWhere) { + sb.append(" AND "); + } else { + sb.append(" WHERE "); + } + } + + /** + * ensure that the WHERE we found was not quoted + * + * @param start + * @param end + * @param querySoFar + * @return true -> not quoted, false -> quoted + */ + private static boolean notQuoted(int start, int end, String querySoFar) { + Matcher quoteMatcher = StringBasedN1qlQueryParser.QUOTE_DETECTION_PATTERN.matcher(querySoFar); + List quotes = new ArrayList(); + while (quoteMatcher.find()) { + quotes.add(new int[] { quoteMatcher.start(), quoteMatcher.end() }); + } + + for (int[] quote : quotes) { + if (quote[0] <= start && quote[1] >= end) { + return false; // it is quoted + } + } + return true; // is not quoted + } + public String export() { StringBuilder sb = new StringBuilder(); - appendWhere(sb); + appendWhere(sb, null); appendSort(sb); appendSkipAndLimit(sb); return sb.toString(); } + public String toN1qlString(ReactiveCouchbaseTemplate template, Class domainClass, boolean isCount) { + StringBasedN1qlQueryParser.N1qlSpelValues n1ql = getN1qlSpelValues(template, domainClass, isCount); + final StringBuilder statement = new StringBuilder(); + appendString(statement, n1ql.selectEntity); // select ... + appendWhereString(statement, n1ql.filter); // typeKey = typeValue + appendWhere(statement, null); // criteria on this Query + appendSort(statement); + appendSkipAndLimit(statement); + return statement.toString(); + } + + StringBasedN1qlQueryParser.N1qlSpelValues getN1qlSpelValues(ReactiveCouchbaseTemplate template, Class domainClass, + boolean isCount) { + String typeKey = template.getConverter().getTypeKey(); + final CouchbasePersistentEntity persistentEntity = template.getConverter().getMappingContext().getRequiredPersistentEntity( + domainClass); + MappingCouchbaseEntityInformation info = new MappingCouchbaseEntityInformation<>(persistentEntity); + String typeValue = info.getJavaType().getName(); + return StringBasedN1qlQueryParser.createN1qlSpelValues(template.getBucketName(), typeKey, typeValue, isCount); + } + + /** + * build QueryOptions forom parameters and scanConsistency + * + * @param scanConsistency + * @return QueryOptions + */ + public QueryOptions buildQueryOptions(QueryScanConsistency scanConsistency) { + final QueryOptions options = QueryOptions.queryOptions(); + if (getParameters() != null) { + if (getParameters() instanceof JsonArray) { + options.parameters((JsonArray) getParameters()); + } else { + options.parameters((JsonObject) getParameters()); + } + } + if (scanConsistency != null) { + options.scanConsistency(scanConsistency); + } + + return options; + } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java index 79da5f46..bb6292ff 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteria.java @@ -22,6 +22,10 @@ import java.util.List; import org.springframework.lang.Nullable; +/** + * @author Michael Nitschinger + * @author Michael Reiche + */ public class QueryCriteria implements QueryCriteriaDefinition { private final String key; @@ -126,29 +130,23 @@ public class QueryCriteria implements QueryCriteriaDefinition { } public QueryCriteria startingWith(@Nullable Object o) { - if (o instanceof QueryCriteria) { - QueryCriteria term = where("\"%\"").plus(o); - like(term); - } else { - like(o.toString() + "%"); - } + operator = "STARTING_WITH"; + value = new Object[] { o }; + format = "%1$s like (%3$s||\"%%\")"; return this; } public QueryCriteria plus(@Nullable Object o) { operator = "PLUS"; value = new Object[] { o }; - format = "(%1$s + %3$s)"; + format = "(%1$s || %3$s)"; return this; } public QueryCriteria endingWith(@Nullable Object o) { - if (o instanceof QueryCriteria) { - QueryCriteria term = where("%").plus(o); - like(term); - } else { - like("%" + o.toString()); - } + operator = "ENDING_WITH"; + value = new Object[] { o }; + format = "%1$s like (\"%%\"||%3$s)"; return this; } @@ -181,10 +179,10 @@ public class QueryCriteria implements QueryCriteriaDefinition { } public QueryCriteria notLike(@Nullable Object o) { - value = new QueryCriteria[] { wrap(like(o)) }; - operator = "NOT"; - format = format = "not( %3$s )"; - return (QueryCriteria) this; + operator = "NOTLIKE"; + value = new Object[] { o }; + format = "not(%1$s like %3$s)"; + return this; } public QueryCriteria isNull() { @@ -277,8 +275,17 @@ public class QueryCriteria implements QueryCriteriaDefinition { return (QueryCriteria) this; } + /** + * This exports the query criteria into a string to be appended to the beginning of an N1QL statement + * + * @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters + * There may already be positional parameters in the beginning of the statement, + * so it may not always start at 1. If it has the value -1, the query is using + * named parameters. If the pointer is null, the query is not using parameters. + * @return string containing part of N1QL query + */ @Override - public String export() { + public String export(int[] paramIndexPtr) { StringBuilder output = new StringBuilder(); boolean first = true; for (QueryCriteria c : this.criteriaChain) { @@ -286,18 +293,29 @@ public class QueryCriteria implements QueryCriteriaDefinition { if (c.chainOperator == null) { throw new IllegalStateException("A chain operator must be present when chaining!"); } - // the consistent place to output this would be in the c.toQueryString(output) about five lines down + // the consistent place to output this would be in the c.exportSingle(output) about five lines down output.append(" ").append(c.chainOperator.representation).append(" "); } else { first = false; } - c.exportSingle(output); + c.exportSingle(output, paramIndexPtr); } return output.toString(); } - private StringBuilder exportSingle(StringBuilder sb) { + /** + * Export the query criteria to a string without using positional or named parameters. + * + * @return string containing part of N1QL query + */ + @Override + public String export() { + return export(null); + + } + + private StringBuilder exportSingle(StringBuilder sb, int[] paramIndexPtr) { String fieldName = maybeQuote(key); int valueLen = value == null ? 0 : value.length; Object[] v = new Object[valueLen + 2]; @@ -305,9 +323,9 @@ public class QueryCriteria implements QueryCriteriaDefinition { v[1] = operator; for (int i = 0; i < valueLen; i++) { if (value[i] instanceof QueryCriteria) { - v[i + 2] = "(" + ((QueryCriteria) value[i]).export() + ")"; + v[i + 2] = "(" + ((QueryCriteria) value[i]).export(paramIndexPtr) + ")"; } else { - v[i + 2] = maybeWrapValue(value[i]); + v[i + 2] = maybeWrapValue(key, value[i], paramIndexPtr); } } @@ -322,7 +340,15 @@ public class QueryCriteria implements QueryCriteriaDefinition { return sb; } - private String maybeWrapValue(Object value) { + private String maybeWrapValue(String key, Object value, int[] paramIndexPtr) { + if (paramIndexPtr != null) { + if (paramIndexPtr[0] >= 0) { + return "$" + (++paramIndexPtr[0]); // these are generated in order + } else { + return "$" + key; + } + } + if (value instanceof String) { return "\"" + value + "\""; } else if (value == null) { diff --git a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteriaDefinition.java b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteriaDefinition.java index 518996ae..6c3e116b 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteriaDefinition.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/QueryCriteriaDefinition.java @@ -18,8 +18,25 @@ package org.springframework.data.couchbase.core.query; /** * @author Oliver Gierke * @author Christoph Strobl + * @author Michael Reiche */ public interface QueryCriteriaDefinition { + /** + * This exports the query criteria into a string to be appended to the beginning of an N1QL statement + * + * @param paramIndexPtr - this is a reference to the parameter index to be used for positional parameters + * There may already be positional parameters in the beginning of the statement, + * so it may not always start at 1. If it has the value -1, the query is using + * named parameters. If the pointer is null, the query is not using parameters. + * @return string containing part of N1QL query + */ + String export(int[] paramIndexPtr); + + /** + * Export the query criteria to a string without using positional or named parameters. + * + * @return string containing part of N1QL query + */ String export(); } diff --git a/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java b/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java new file mode 100644 index 00000000..477de3ea --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/query/StringQuery.java @@ -0,0 +1,76 @@ +/* + * Copyright 2017-2020 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 + * + * https://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.core.query; + +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonValue; +import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; +import org.springframework.data.couchbase.repository.query.StringBasedN1qlQueryParser; + +/** + * @author Michael Reiche + * + * Query created from the string in @Query annotation in the repository interface. + * + * @Query("#{#n1ql.selectEntity} where firstname = $1 and lastname = $2") + * List getByFirstnameAndLastname(String firstname, String lastname); + * + * It must include the SELECT ... FROM ... preferably via the #n1ql expression + * In addition to any predicates in the string, a predicate for the domainType (class) + * will be added. + */ +public class StringQuery extends Query { + + private String inlineN1qlQuery; + + public StringQuery(String n1qlString) { + inlineN1qlQuery = n1qlString; + } + + /** + * inlineN1qlQuery (Query Annotation) + * append the string query to the provided StringBuilder. + * To be used along with the other append*() methods to construct the N1QL statement + * @param sb - StringBuilder + */ + private void appendInlineN1qlStatement(final StringBuilder sb) { + sb.append(inlineN1qlQuery); + } + + @Override + public String toN1qlString(ReactiveCouchbaseTemplate template, Class domainClass, boolean isCount) { + StringBasedN1qlQueryParser.N1qlSpelValues n1ql = getN1qlSpelValues(template, domainClass, isCount); + final StringBuilder statement = new StringBuilder(); + appendInlineN1qlStatement(statement); // apply the string statement + appendWhereString(statement, n1ql.filter); // typeKey = typeValue + + // To use generated parameters for literals + // we need to figure out if we must use positional or named parameters + // If we are using positional parameters, we need to start where + // the inlineN1ql left off. + int[] paramIndexPtr = null; + JsonValue params = this.getParameters(); + if (params instanceof JsonArray) { // positional parameters + paramIndexPtr = new int[] { ((JsonArray) params).size() }; + } else { // named parameters or no parameters, no index required + paramIndexPtr = new int[] { -1 }; + } + appendWhere(statement, paramIndexPtr); // criteria on this Query - should be empty for StringQuery + appendSort(statement); + appendSkipAndLimit(statement); + return statement.toString(); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/core/support/TemplateUtils.java b/src/main/java/org/springframework/data/couchbase/core/support/TemplateUtils.java index 0aadd6da..67933772 100644 --- a/src/main/java/org/springframework/data/couchbase/core/support/TemplateUtils.java +++ b/src/main/java/org/springframework/data/couchbase/core/support/TemplateUtils.java @@ -25,11 +25,13 @@ import org.springframework.data.couchbase.core.OperationInterruptedException; /** * @author Subhashni Balakrishnan + * @author Michael Reiche * @since 3.0 */ public class TemplateUtils { - public static final String SELECT_ID = "_ID"; - public static final String SELECT_CAS = "_CAS"; + public static final String SELECT_ID = "__id"; + public static final String SELECT_CAS = "__cas"; + public static final String SELECT_COUNT = "__count"; private static PersistenceExceptionTranslator exceptionTranslator = new CouchbaseExceptionTranslator(); public static Throwable translateError(Throwable e) { diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuery.java index 7666f463..bef5d516 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuery.java @@ -16,22 +16,30 @@ package org.springframework.data.couchbase.repository.query; import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; +/** + * @author Michael Nitschinger + * @author Michael Reiche + */ public class CouchbaseRepositoryQuery implements RepositoryQuery { private final CouchbaseOperations operations; - private final QueryMethod queryMethod; + private final CouchbaseQueryMethod queryMethod; + private final NamedQueries namedQueries; - public CouchbaseRepositoryQuery(final CouchbaseOperations operations, final QueryMethod queryMethod) { + public CouchbaseRepositoryQuery(final CouchbaseOperations operations, final CouchbaseQueryMethod queryMethod, + final NamedQueries namedQueries) { this.operations = operations; this.queryMethod = queryMethod; + this.namedQueries = namedQueries; } @Override public Object execute(final Object[] parameters) { - return new N1qlRepositoryQueryExecutor(operations, queryMethod).execute(parameters); + return new N1qlRepositoryQueryExecutor(operations, queryMethod, namedQueries).execute(parameters); } @Override 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 index fc503c40..54b7d6bc 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreator.java @@ -17,34 +17,50 @@ package org.springframework.data.couchbase.repository.query; import static org.springframework.data.couchbase.core.query.QueryCriteria.*; +import java.lang.reflect.Array; import java.util.Iterator; +import com.couchbase.client.core.error.InvalidArgumentException; +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonValue; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; +/** + * @author Michael Nitschinger + * @author Michael Reiche + */ public class N1qlQueryCreator extends AbstractQueryCreator { private final ParameterAccessor accessor; private final MappingContext context; + private final QueryMethod queryMethod; + private final CouchbaseConverter converter; - public N1qlQueryCreator(final PartTree tree, final ParameterAccessor accessor, - final MappingContext context) { + public N1qlQueryCreator(final PartTree tree, final ParameterAccessor accessor, QueryMethod queryMethod, + CouchbaseConverter converter) { super(tree, accessor); this.accessor = accessor; - this.context = context; + this.context = converter.getMappingContext(); + this.queryMethod = queryMethod; + this.converter = converter; } @Override protected QueryCriteria create(final Part part, final Iterator iterator) { - PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); + PersistentPropertyPath path = context.getPersistentPropertyPath( + part.getProperty()); CouchbasePersistentProperty property = path.getLeafProperty(); return from(part, property, where(path.toDotPath()), iterator); } @@ -55,7 +71,8 @@ public class N1qlQueryCreator extends AbstractQueryCreator return create(part, iterator); } - PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); + PersistentPropertyPath path = context.getPersistentPropertyPath( + part.getProperty()); CouchbasePersistentProperty property = path.getLeafProperty(); return from(part, property, base.and(path.toDotPath()), iterator); @@ -68,71 +85,92 @@ public class N1qlQueryCreator extends AbstractQueryCreator @Override protected Query complete(QueryCriteria criteria, Sort sort) { - return (criteria == null ? new Query() : new Query().addCriteria(criteria)).with(sort); + JsonArray params = (JsonArray) getPositionalPlaceholderValues(accessor); + return (criteria == null ? new Query() : new Query().addCriteria(criteria)).with(sort).setPositionalParameters( + params); } - private QueryCriteria from(final Part part, final CouchbasePersistentProperty property, final QueryCriteria criteria, - final Iterator parameters) { + private QueryCriteria from(final Part part, final CouchbasePersistentProperty property, + final QueryCriteria criteria, final Iterator parameters) { final Part.Type type = part.getType(); /* NEAR(new String[]{"IsNear", "Near"}), - WITHIN(new String[]{"IsWithin", "Within"}), */ switch (type) { - case GREATER_THAN: - case AFTER: - return criteria.gt(parameters.next()); - case GREATER_THAN_EQUAL: - return criteria.gte(parameters.next()); - case LESS_THAN: - case BEFORE: - return criteria.lt(parameters.next()); - case LESS_THAN_EQUAL: - return criteria.lte(parameters.next()); - case SIMPLE_PROPERTY: - return criteria.eq(parameters.next()); - case NEGATING_SIMPLE_PROPERTY: - return criteria.ne(parameters.next()); - case CONTAINING: - return criteria.containing(parameters.next()); - case NOT_CONTAINING: - return criteria.notContaining(parameters.next()); - case STARTING_WITH: - return criteria.startingWith(parameters.next()); - case ENDING_WITH: - return criteria.endingWith(parameters.next()); - case LIKE: - return criteria.like(parameters.next()); - case NOT_LIKE: - return criteria.notLike(parameters.next()); - case WITHIN: - return criteria.within(parameters.next()); - case IS_NULL: - return criteria.isNull(/*parameters.next()*/); - case IS_NOT_NULL: - return criteria.isNotNull(/*parameters.next()*/); - case IS_EMPTY: - return criteria.isNotValued(/*parameters.next()*/); - case IS_NOT_EMPTY: - return criteria.isValued(/*parameters.next()*/); - case EXISTS: - return criteria.isNotMissing(/*parameters.next()*/); - case REGEX: - return criteria.regex(parameters.next()); - case BETWEEN: - return criteria.between(parameters.next(), parameters.next()); - case IN: - return criteria.in((Object[]) parameters.next()); - case NOT_IN: - return criteria.notIn((Object[]) parameters.next()); - case TRUE: - return criteria.TRUE(); - case FALSE: - return criteria.FALSE(); - default: - throw new IllegalArgumentException("Unsupported keyword!"); + case GREATER_THAN: + case AFTER: + return criteria.gt(parameters.next()); + case GREATER_THAN_EQUAL: + return criteria.gte(parameters.next()); + case LESS_THAN: + case BEFORE: + return criteria.lt(parameters.next()); + case LESS_THAN_EQUAL: + return criteria.lte(parameters.next()); + case SIMPLE_PROPERTY: + return criteria.eq(parameters.next()); + case NEGATING_SIMPLE_PROPERTY: + return criteria.ne(parameters.next()); + case CONTAINING: + return criteria.containing(parameters.next()); + case NOT_CONTAINING: + return criteria.notContaining(parameters.next()); + case STARTING_WITH: + return criteria.startingWith(parameters.next()); + case ENDING_WITH: + return criteria.endingWith(parameters.next()); + case LIKE: + return criteria.like(parameters.next()); + case NOT_LIKE: + return criteria.notLike(parameters.next()); + case WITHIN: + return criteria.within(parameters.next()); + case IS_NULL: + return criteria.isNull(/*parameters.next()*/); + case IS_NOT_NULL: + return criteria.isNotNull(/*parameters.next()*/); + case IS_EMPTY: + return criteria.isNotValued(/*parameters.next()*/); + case IS_NOT_EMPTY: + return criteria.isValued(/*parameters.next()*/); + case EXISTS: + return criteria.isNotMissing(/*parameters.next()*/); + case REGEX: + return criteria.regex(parameters.next()); + case BETWEEN: + return criteria.between(parameters.next(), parameters.next()); + case IN: + return criteria.in((Object[]) parameters.next()); + case NOT_IN: + return criteria.notIn((Object[]) parameters.next()); + case TRUE: + return criteria.TRUE(); + case FALSE: + return criteria.FALSE(); + default: + throw new IllegalArgumentException("Unsupported keyword!"); } } + // from StringN1qlQueryParser + private JsonValue getPositionalPlaceholderValues(ParameterAccessor accessor) { + JsonArray posValues = JsonArray.create(); + if (queryMethod == null) + return posValues; + for (Parameter parameter : this.queryMethod.getParameters().getBindableParameters()) { + try { + posValues.add(converter.convertForWriteIfNeeded(accessor.getBindableValue(parameter.getIndex()))); + } catch (InvalidArgumentException iae) { + Object o = accessor.getBindableValue(parameter.getIndex()); + if (o instanceof Object[]) { + Object[] array = (Object[]) o; + for (Object e : array) { + posValues.add(converter.convertForWriteIfNeeded(e)); + } + } + } + } + return posValues; + } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java index 5151bb70..b90a0b2a 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/N1qlRepositoryQueryExecutor.java @@ -15,33 +15,64 @@ */ package org.springframework.data.couchbase.repository.query; +import java.util.ArrayList; import java.util.List; import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation; import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.parser.PartTree; +/** + * @author Michael Nitschinger + * @author Michael Reiche + */ public class N1qlRepositoryQueryExecutor { private final CouchbaseOperations operations; - private final QueryMethod queryMethod; + private final CouchbaseQueryMethod queryMethod; + private final NamedQueries namedQueries; - public N1qlRepositoryQueryExecutor(final CouchbaseOperations operations, final QueryMethod queryMethod) { + public N1qlRepositoryQueryExecutor(final CouchbaseOperations operations, final CouchbaseQueryMethod queryMethod, + final NamedQueries namedQueries) { this.operations = operations; this.queryMethod = queryMethod; + this.namedQueries = namedQueries; } + /** + * see also {@link ReactiveN1qlRepositoryQueryExecutor#execute(Object[] parameters) execute } + * + * @param parameters + * @return + */ public Object execute(final Object[] parameters) { final Class domainClass = queryMethod.getResultProcessor().getReturnedType().getDomainType(); final ParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); - final PartTree tree = new PartTree(queryMethod.getName(), domainClass); - Query query = new N1qlQueryCreator(tree, accessor, operations.getConverter().getMappingContext()).createQuery(); + // this is identical to ReactiveN1qlRespositoryQueryExecutor, + // except for the type of 'q', and the call to oneValue() vs one() + Query query; + ExecutableFindByQueryOperation.ExecutableFindByQuery q; + if (queryMethod.hasN1qlAnnotation()) { + query = new StringN1qlQueryCreator(accessor, queryMethod, operations.getConverter(), + operations.getBucketName(), QueryMethodEvaluationContextProvider.DEFAULT, + namedQueries).createQuery(); + } else { + final PartTree tree = new PartTree(queryMethod.getName(), domainClass); + query = new N1qlQueryCreator(tree, accessor, queryMethod, operations.getConverter()).createQuery(); + } + q = (ExecutableFindByQueryOperation.ExecutableFindByQuery) operations.findByQuery(domainClass).matching(query); + if (queryMethod.isCollectionQuery()) { + return q.all(); + } else { + return q.oneValue(); + } - List all = operations.findByQuery(domainClass).matching(query).all(); - return all; } + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQuery.java index 0fb44ae7..128a89e6 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveCouchbaseRepositoryQuery.java @@ -16,22 +16,30 @@ package org.springframework.data.couchbase.repository.query; import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; +import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; +/** + * @author Michael Nitschinger + * @author Michael Reiche + */ public class ReactiveCouchbaseRepositoryQuery implements RepositoryQuery { private final ReactiveCouchbaseOperations operations; - private final QueryMethod queryMethod; + private final CouchbaseQueryMethod queryMethod; + private final NamedQueries namedQueries; - public ReactiveCouchbaseRepositoryQuery(final ReactiveCouchbaseOperations operations, final QueryMethod queryMethod) { + public ReactiveCouchbaseRepositoryQuery(final ReactiveCouchbaseOperations operations, + final CouchbaseQueryMethod queryMethod, final NamedQueries namedQueries) { this.operations = operations; this.queryMethod = queryMethod; + this.namedQueries = namedQueries; } @Override public Object execute(final Object[] parameters) { - return new ReactiveN1qlRepositoryQueryExecutor(operations, queryMethod).execute(parameters); + return new ReactiveN1qlRepositoryQueryExecutor(operations, queryMethod, namedQueries).execute(parameters); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveN1qlRepositoryQueryExecutor.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveN1qlRepositoryQueryExecutor.java index 6981eda6..e000f25c 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveN1qlRepositoryQueryExecutor.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveN1qlRepositoryQueryExecutor.java @@ -15,35 +15,67 @@ */ package org.springframework.data.couchbase.repository.query; +import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation; +import org.springframework.data.couchbase.core.ReactiveFindByQueryOperation; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.query.*; +import org.springframework.expression.spel.standard.SpelExpressionParser; import reactor.core.publisher.Flux; import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations; import org.springframework.data.couchbase.core.query.Query; -import org.springframework.data.repository.query.ParameterAccessor; -import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.parser.PartTree; +import java.util.List; + +/** + * @author Michael Nitschinger + * @author Michael Reiche + */ public class ReactiveN1qlRepositoryQueryExecutor { private final ReactiveCouchbaseOperations operations; - private final QueryMethod queryMethod; + private final CouchbaseQueryMethod queryMethod; + private final NamedQueries namedQueries; public ReactiveN1qlRepositoryQueryExecutor(final ReactiveCouchbaseOperations operations, - final QueryMethod queryMethod) { + final CouchbaseQueryMethod queryMethod, final NamedQueries namedQueries) { this.operations = operations; this.queryMethod = queryMethod; + this.namedQueries = namedQueries; } + /** + * see also {@link N1qlRepositoryQueryExecutor#execute(Object[] parameters) execute } + * + * @param parameters + * @return + */ public Object execute(final Object[] parameters) { final Class domainClass = queryMethod.getResultProcessor().getReturnedType().getDomainType(); final ParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); + final String namedQueryName = queryMethod.getNamedQueryName(); - final PartTree tree = new PartTree(queryMethod.getName(), domainClass); - Query query = new N1qlQueryCreator(tree, accessor, operations.getConverter().getMappingContext()).createQuery(); + // this is identical to ExecutableN1qlRespositoryQueryExecutor, + // except for the type of 'q', and the call to one() vs oneValue() - Flux all = operations.findByQuery(domainClass).matching(query).all(); - return all; + Query query; + ReactiveFindByQueryOperation.ReactiveFindByQuery q; + if (queryMethod.hasN1qlAnnotation()) { + query = new StringN1qlQueryCreator(accessor, queryMethod, operations.getConverter(), + operations.getBucketName(), QueryMethodEvaluationContextProvider.DEFAULT, + namedQueries).createQuery(); + } else { + final PartTree tree = new PartTree(queryMethod.getName(), domainClass); + query = new N1qlQueryCreator(tree, accessor, queryMethod, operations.getConverter()).createQuery(); + } + q = (ReactiveFindByQueryOperation.ReactiveFindByQuery) operations.findByQuery(domainClass).matching(query); + if (queryMethod.isCollectionQuery()) { + return q.all(); + } else { + return q.one(); + } } } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringN1qlBasedQuery.java index 50a7af59..5a1058a3 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ReactiveStringN1qlBasedQuery.java @@ -61,8 +61,8 @@ public class ReactiveStringN1qlBasedQuery extends ReactiveAbstractN1qlBasedQuery return getCouchbaseOperations().getConverter().getTypeKey(); } - protected Class getTypeValue() { - return getQueryMethod().getEntityInformation().getJavaType(); + protected String getTypeValue() { + return getQueryMethod().getEntityInformation().getJavaType().getName(); } @Override diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java index 151746d7..aea20d75 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringBasedN1qlQueryParser.java @@ -19,10 +19,13 @@ import static org.springframework.data.couchbase.core.query.N1QLExpression.*; import static org.springframework.data.couchbase.core.support.TemplateUtils.*; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.couchbase.client.core.error.InvalidArgumentException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; @@ -42,6 +45,7 @@ import com.couchbase.client.java.json.JsonValue; /** * @author Subhashni Balakrishnan + * @author Michael Reiche */ public class StringBasedN1qlQueryParser { public static final String SPEL_PREFIX = "n1ql"; @@ -84,11 +88,17 @@ public class StringBasedN1qlQueryParser { * the statement. */ public static final String SPEL_RETURNING = "#" + SPEL_PREFIX + ".returning"; - /** regexp that detect $named placeholder (starts with a letter, then alphanum chars) */ + /** + * regexp that detect $named placeholder (starts with a letter, then alphanum chars) + */ public static final Pattern NAMED_PLACEHOLDER_PATTERN = Pattern.compile("\\W(\\$\\p{Alpha}\\p{Alnum}*)\\b"); - /** regexp that detect positional placeholder ($ followed by digits only) */ + /** + * regexp that detect positional placeholder ($ followed by digits only) + */ public static final Pattern POSITIONAL_PLACEHOLDER_PATTERN = Pattern.compile("\\W(\\$\\p{Digit}+)\\b"); - /** regexp that detects " and ' quote boundaries, ignoring escaped quotes */ + /** + * regexp that detects " and ' quote boundaries, ignoring escaped quotes + */ public static final Pattern QUOTE_DETECTION_PATTERN = Pattern.compile("[\"'](?:[^\"'\\\\]*(?:\\\\.)?)*[\"']"); private static final Logger LOGGER = LoggerFactory.getLogger(StringBasedN1qlQueryParser.class); private final String statement; @@ -97,9 +107,10 @@ public class StringBasedN1qlQueryParser { private final N1qlSpelValues statementContext; private final N1qlSpelValues countContext; private final CouchbaseConverter couchbaseConverter; + private final Collection parameterNames = new HashSet(); public StringBasedN1qlQueryParser(String statement, QueryMethod queryMethod, String bucketName, - CouchbaseConverter couchbaseConverter, String typeField, Class typeValue) { + CouchbaseConverter couchbaseConverter, String typeField, String typeValue) { this.statement = statement; this.queryMethod = queryMethod; this.placeHolderType = checkPlaceholders(statement); @@ -108,7 +119,7 @@ public class StringBasedN1qlQueryParser { this.couchbaseConverter = couchbaseConverter; } - public static N1qlSpelValues createN1qlSpelValues(String bucketName, String typeField, Class typeValue, + public static N1qlSpelValues createN1qlSpelValues(String bucketName, String typeField, String typeValue, boolean isCount) { String b = "`" + bucketName + "`"; String entity = "META(" + b + ").id AS " + SELECT_ID + ", META(" + b + ").cas AS " + SELECT_CAS; @@ -119,7 +130,7 @@ public class StringBasedN1qlQueryParser { } else { selectEntity = "SELECT " + entity + ", " + b + ".* FROM " + b; } - String typeSelection = "`" + typeField + "` = \"" + typeValue.getName() + "\""; + String typeSelection = "`" + typeField + "` = \"" + typeValue + "\""; String delete = N1QLExpression.delete().from(i(bucketName)).toString(); String returning = " returning " + N1qlUtils.createReturningExpressionForDelete(bucketName).toString(); @@ -141,6 +152,7 @@ public class StringBasedN1qlQueryParser { } private PlaceholderType checkPlaceholders(String statement) { + Matcher quoteMatcher = QUOTE_DETECTION_PATTERN.matcher(statement); Matcher positionMatcher = POSITIONAL_PLACEHOLDER_PATTERN.matcher(statement); Matcher namedMatcher = NAMED_PLACEHOLDER_PATTERN.matcher(statement); @@ -159,6 +171,7 @@ public class StringBasedN1qlQueryParser { if (checkNotQuoted(placeholder, positionMatcher.start(), positionMatcher.end(), quotes)) { LOGGER.trace("{}: Found positional placeholder {}", this.queryMethod.getName(), placeholder); posCount++; + parameterNames.add(placeholder.substring(1)); // save without the leading $ } } @@ -168,13 +181,17 @@ public class StringBasedN1qlQueryParser { if (checkNotQuoted(placeholder, namedMatcher.start(), namedMatcher.end(), quotes)) { LOGGER.trace("{}: Found named placeholder {}", this.queryMethod.getName(), placeholder); namedCount++; + parameterNames.add(placeholder.substring(1));//save without the leading $ } } - if (posCount > 0 && namedCount > 0) { + if (posCount > 0 && namedCount > 0) { // actual values from parameterNames might be more useful throw new IllegalArgumentException("Using both named (" + namedCount + ") and positional (" + posCount - + ") placeholders is not supported, please choose one over the other in " + this.queryMethod.getName()); - } else if (posCount > 0) { + + ") placeholders is not supported, please choose one over the other in " + + queryMethod.getClass().getName() + "." + this.queryMethod.getName() + "()"); + } + + if (posCount > 0) { return PlaceholderType.POSITIONAL; } else if (namedCount > 0) { return PlaceholderType.NAMED; @@ -186,50 +203,127 @@ public class StringBasedN1qlQueryParser { private boolean checkNotQuoted(String item, int start, int end, List quotes) { for (int[] quote : quotes) { if (quote[0] <= start && quote[1] >= end) { - LOGGER.trace("{}: potential placeholder {} is inside quotes, ignored", this.queryMethod.getName(), item); + LOGGER.trace("{}: potential placeholder {} is inside quotes, ignored", this.queryMethod.getName(), + item); return false; } } return true; } + /** + * Get Postional argument placeholders to use for parameters. $1, $2 etc. + * + * @param accessor + * @return - JsonValue holding parameters. + */ private JsonValue getPositionalPlaceholderValues(ParameterAccessor accessor) { JsonArray posValues = JsonArray.create(); for (Parameter parameter : this.queryMethod.getParameters().getBindableParameters()) { - posValues.add(this.couchbaseConverter.convertForWriteIfNeeded(accessor.getBindableValue(parameter.getIndex()))); + Object rawValue = accessor.getBindableValue(parameter.getIndex()); + Object value = couchbaseConverter.convertForWriteIfNeeded(rawValue); + putPositionalValue(accessor, parameter, posValues, value); } return posValues; } + /** + * Get Named argument placeholders to use for parameters. $lastname, $city etc. + * + * @param accessor + * @return + */ private JsonObject getNamedPlaceholderValues(ParameterAccessor accessor) { JsonObject namedValues = JsonObject.create(); - + HashSet pNames = new HashSet<>(parameterNames); for (Parameter parameter : this.queryMethod.getParameters().getBindableParameters()) { String placeholder = parameter.getPlaceholder(); - Object value = this.couchbaseConverter.convertForWriteIfNeeded(accessor.getBindableValue(parameter.getIndex())); + Object rawValue = accessor.getBindableValue(parameter.getIndex()); + Object value = couchbaseConverter.convertForWriteIfNeeded(rawValue); if (placeholder != null && placeholder.charAt(0) == ':') { placeholder = placeholder.replaceFirst(":", ""); - namedValues.put(placeholder, value); + putNamedValue(accessor, parameter, namedValues, placeholder, value); + if (pNames.contains(placeholder)) { + pNames.remove(placeholder); + } else { + throw new RuntimeException( + "parameter named " + placeholder + " does not match any named parameter " + parameterNames + + " in " + statement); + } } else { - parameter.getName().ifPresent(name -> namedValues.put(name, value)); + if (parameter.getName().isPresent()) { + putNamedValue(accessor, parameter, namedValues, parameter.getName().get(), value); + } else { + throw new RuntimeException( + "cannot determine argument for named parameter. " + "Argument " + parameter.getIndex() + + " to " + queryMethod.getClass().getName() + "." + queryMethod.getName() + + "() needs @Param(\"name\") that matches a named parameter in " + statement); + } } } + if (!pNames.isEmpty()) { + throw new RuntimeException("no parameter found for " + pNames); + } return namedValues; } protected JsonValue getPlaceholderValues(ParameterAccessor accessor) { switch (this.placeHolderType) { - case NAMED: - return getNamedPlaceholderValues(accessor); - case POSITIONAL: - return getPositionalPlaceholderValues(accessor); - case NONE: - default: - return JsonArray.create(); + case NAMED: + return getNamedPlaceholderValues(accessor); + case POSITIONAL: + return getPositionalPlaceholderValues(accessor); + case NONE: + default: + return JsonArray.create(); } } + private void putPositionalValue(ParameterAccessor accessor, Parameter parameter, JsonArray posValues, + Object value) { + try { + posValues.add(value); + } catch (InvalidArgumentException iae) { + if (value instanceof Object[]) { // Maybe just need to treat it as an array ? + addAsArray(posValues, value); + } else { + throw iae; + } + } + } + + private void addAsArray(JsonArray posValues, Object o) { + Object[] array = (Object[]) o; + JsonArray ja = JsonValue.ja(); + for (Object e : array) { + ja.add(String.valueOf(couchbaseConverter.convertForWriteIfNeeded(e))); + } + posValues.add(ja); + } + + private void putNamedValue(ParameterAccessor accessor, Parameter parameter, JsonObject namedValues, + String placeholder, Object value) { + try { + namedValues.put(placeholder, value); + } catch (InvalidArgumentException iae) { + if (value instanceof Object[]) { // Maybe just need to treat it as an array? + addAsArray(namedValues, placeholder, value); + } else { + throw iae; + } + } + } + + private void addAsArray(JsonObject namedValues, String placeholder, Object o) { + Object[] array = (Object[]) o; + JsonArray ja = JsonValue.ja(); + for (Object e : array) { + ja.add(String.valueOf(couchbaseConverter.convertForWriteIfNeeded(e))); + } + namedValues.put(placeholder, ja); + } + protected boolean useGeneratedCountQuery() { return this.statement.contains(SPEL_SELECT_FROM_CLAUSE); } @@ -246,7 +340,9 @@ public class StringBasedN1qlQueryParser { return this.statement; } - /** enumeration of all the combinations of placeholder types that could be found in a N1QL statement */ + /** + * enumeration of all the combinations of placeholder types that could be found in a N1QL statement + */ private enum PlaceholderType { NAMED, POSITIONAL, NONE } @@ -306,4 +402,5 @@ public class StringBasedN1qlQueryParser { this.returning = returning; } } + } 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 14e7793f..15456d56 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 @@ -60,7 +60,7 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery { QueryMethodEvaluationContextProvider evaluationContextProvider) { super(queryMethod, couchbaseOperations); this.queryParser = new StringBasedN1qlQueryParser(statement, queryMethod, getCouchbaseOperations().getBucketName(), - getCouchbaseOperations().getConverter(), getTypeField(), getTypeValue()); + getCouchbaseOperations().getConverter(), getTypeField(), getTypeValue().getName()); this.parser = spelParser; this.evaluationContextProvider = evaluationContextProvider; } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java new file mode 100644 index 00000000..05213c45 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreator.java @@ -0,0 +1,189 @@ +/* + * Copyright 2020 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 + * + * https://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 org.springframework.data.couchbase.core.query.N1QLExpression.x; +import static org.springframework.data.couchbase.core.query.QueryCriteria.*; + +import java.util.ArrayList; +import java.util.Iterator; + +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonObject; +import com.couchbase.client.java.json.JsonValue; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.query.N1QLExpression; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.core.query.StringQuery; +import org.springframework.data.couchbase.repository.query.support.N1qlUtils; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.query.*; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; + +/** + * @author Michael Reiche + */ +public class StringN1qlQueryCreator extends AbstractQueryCreator { + + private final ParameterAccessor accessor; + private final MappingContext context; + private final SpelExpressionParser parser; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final StringBasedN1qlQueryParser queryParser; + private final QueryMethod queryMethod; + private final CouchbaseConverter couchbaseConverter; + private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser(); + + public StringN1qlQueryCreator(final ParameterAccessor accessor, CouchbaseQueryMethod queryMethod, + CouchbaseConverter couchbaseConverter, String bucketName, + QueryMethodEvaluationContextProvider evaluationContextProvider, NamedQueries namedQueries) { + + // AbstractQueryCreator needs a PartTree, so we give it a dummy one. + // The resulting dummy criteria will not be included in the Query + // by {@link #complete((QueryCriteria criteria, Sort sort)) complete} + super(new PartTree("dummy", (new Object() { + String dummy; + }).getClass()), accessor); + this.accessor = accessor; + this.context = couchbaseConverter.getMappingContext(); + this.queryMethod = queryMethod; + this.couchbaseConverter = couchbaseConverter; + this.evaluationContextProvider = evaluationContextProvider; + final String namedQueryName = queryMethod.getNamedQueryName(); + String queryString; + if (queryMethod.hasInlineN1qlQuery()) { + queryString = queryMethod.getInlineN1qlQuery(); + } else if (namedQueries.hasQuery(namedQueryName)) { + queryString = namedQueries.getQuery(namedQueryName); + } else { + throw new IllegalArgumentException("query has no inline Query or named Query not found"); + } + this.queryParser = new StringBasedN1qlQueryParser(queryString, queryMethod, bucketName, couchbaseConverter, + getTypeField(), getTypeValue()); + this.parser = SPEL_PARSER; + } + + protected QueryMethod getQueryMethod() { + return queryMethod; + } + + protected String getTypeField() { + return couchbaseConverter.getTypeKey(); + } + + protected String getTypeValue() { + return getQueryMethod().getEntityInformation().getJavaType().getName(); + } + + @Override + protected QueryCriteria create(final Part part, final Iterator iterator) { + PersistentPropertyPath path = context.getPersistentPropertyPath( + part.getProperty()); + CouchbasePersistentProperty property = path.getLeafProperty(); + return from(part, property, where(path.toDotPath()), iterator); + } + + @Override + protected QueryCriteria and(final Part part, final QueryCriteria base, final Iterator iterator) { + if (base == null) { + return create(part, iterator); + } + + PersistentPropertyPath path = context.getPersistentPropertyPath( + part.getProperty()); + CouchbasePersistentProperty property = path.getLeafProperty(); + + return from(part, property, base.and(path.toDotPath()), iterator); + } + + @Override + protected QueryCriteria or(QueryCriteria base, QueryCriteria criteria) { + return base.or(criteria); + } + + @Override + protected Query complete(QueryCriteria criteria, Sort sort) { + N1QLExpression parsedExpression = getExpression(accessor, getParameters(accessor), null /* returnedType */); + Query q = new StringQuery(parsedExpression.toString()).with(sort); + JsonValue params = queryParser.getPlaceholderValues(accessor); + if (params instanceof JsonArray) { + q.setPositionalParameters((JsonArray) params); + } else { + q.setNamedParameters((JsonObject) params); + } + return q; + } + + private QueryCriteria from(final Part part, final CouchbasePersistentProperty property, + final QueryCriteria criteria, final Iterator parameters) { + + final Part.Type type = part.getType(); + switch (type) { + case SIMPLE_PROPERTY: + return criteria; //.eq(parameters.next()); // this will be the dummy from PartTree + default: + throw new IllegalArgumentException("Unsupported keyword!"); + } + } + + // copied from StringN1qlBasedQuery + private N1QLExpression getExpression(ParameterAccessor accessor, Object[] runtimeParameters, + ReturnedType returnedType) { + EvaluationContext evaluationContext = evaluationContextProvider.getEvaluationContext( + getQueryMethod().getParameters(), runtimeParameters); + N1QLExpression parsedStatement = x(this.queryParser.doParse(parser, evaluationContext, false)); + + Sort sort = accessor.getSort(); + if (sort.isSorted()) { + N1QLExpression[] cbSorts = N1qlUtils.createSort(sort); + parsedStatement = parsedStatement.orderBy(cbSorts); + } + if (queryMethod.isPageQuery()) { + Pageable pageable = accessor.getPageable(); + Assert.notNull(pageable, "Pageable must not be null!"); + parsedStatement = parsedStatement.limit(pageable.getPageSize()).offset( + Math.toIntExact(pageable.getOffset())); + } else if (queryMethod.isSliceQuery()) { + Pageable pageable = accessor.getPageable(); + Assert.notNull(pageable, "Pageable must not be null!"); + parsedStatement = parsedStatement.limit(pageable.getPageSize() + 1).offset( + Math.toIntExact(pageable.getOffset())); + } + return parsedStatement; + } + + // getExpression() could do this itself, but pass as an arg to be consistent with StringN1qlBasedQuery + private static Object[] getParameters(ParameterAccessor accessor) { + ArrayList params = new ArrayList<>(); + for (Object o : accessor) { + params.add(o); + } + return params.toArray(); + } +} + + 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 f00c0661..747c9d76 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 @@ -25,6 +25,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping; import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; +import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod; import org.springframework.data.couchbase.repository.query.CouchbaseRepositoryQuery; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; @@ -33,7 +34,6 @@ 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.QueryLookupStrategy; -import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -46,6 +46,7 @@ import org.springframework.util.Assert; * @author Simon Baslé * @author Oliver Gierke * @author Mark Paluch + * @author Michael Reiche */ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { @@ -88,14 +89,14 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { * Returns entity information based on the domain class. * * @param domainClass the class for the entity. - * @param the value type - * @param the id type. + * @param the value type + * @param the id type. * @return entity information for that domain class. */ @Override public CouchbaseEntityInformation getEntityInformation(Class domainClass) { - CouchbasePersistentEntity entity = (CouchbasePersistentEntity) mappingContext - .getRequiredPersistentEntity(domainClass); + CouchbasePersistentEntity entity = (CouchbasePersistentEntity) mappingContext.getRequiredPersistentEntity( + domainClass); return new MappingCouchbaseEntityInformation<>(entity); } @@ -151,10 +152,11 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { @Override public RepositoryQuery resolveQuery(final Method method, final RepositoryMetadata metadata, final ProjectionFactory factory, final NamedQueries namedQueries) { - final CouchbaseOperations couchbaseOperations = couchbaseOperationsMapping - .resolve(metadata.getRepositoryInterface(), metadata.getDomainType()); + final CouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve( + metadata.getRepositoryInterface(), metadata.getDomainType()); - return new CouchbaseRepositoryQuery(couchbaseOperations, new QueryMethod(method, metadata, factory)); + CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, factory, mappingContext); + return new CouchbaseRepositoryQuery(couchbaseOperations, queryMethod, namedQueries); /*CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, factory, mappingContext); String namedQueryName = queryMethod.getNamedQueryName(); diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactory.java b/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactory.java index 54c9db63..5cc49c1f 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactory.java +++ b/src/main/java/org/springframework/data/couchbase/repository/support/ReactiveCouchbaseRepositoryFactory.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.config.ReactiveRepositoryOperationsMapping; import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; +import org.springframework.data.couchbase.repository.query.CouchbaseQueryMethod; import org.springframework.data.couchbase.repository.query.ReactiveCouchbaseRepositoryQuery; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; @@ -32,7 +33,6 @@ import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport; import org.springframework.data.repository.query.QueryLookupStrategy; -import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -41,6 +41,7 @@ import org.springframework.util.Assert; /** * @author Subhashni Balakrishnan * @author Mark Paluch + * @author Michael Reiche * @since 3.0 */ public class ReactiveCouchbaseRepositoryFactory extends ReactiveRepositoryFactorySupport { @@ -83,14 +84,14 @@ public class ReactiveCouchbaseRepositoryFactory extends ReactiveRepositoryFactor * Returns entity information based on the domain class. * * @param domainClass the class for the entity. - * @param the value type - * @param the id type. + * @param the value type + * @param the id type. * @return entity information for that domain class. */ @Override public CouchbaseEntityInformation getEntityInformation(Class domainClass) { - CouchbasePersistentEntity entity = (CouchbasePersistentEntity) mappingContext - .getRequiredPersistentEntity(domainClass); + CouchbasePersistentEntity entity = (CouchbasePersistentEntity) mappingContext.getRequiredPersistentEntity( + domainClass); return new MappingCouchbaseEntityInformation<>(entity); } @@ -105,8 +106,8 @@ public class ReactiveCouchbaseRepositoryFactory extends ReactiveRepositoryFactor */ @Override protected final Object getTargetRepository(final RepositoryInformation metadata) { - ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping - .resolve(metadata.getRepositoryInterface(), metadata.getDomainType()); + ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve( + metadata.getRepositoryInterface(), metadata.getDomainType()); CouchbaseEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); SimpleReactiveCouchbaseRepository repository = getTargetRepositoryViaReflection(metadata, entityInformation, couchbaseOperations); @@ -148,10 +149,10 @@ public class ReactiveCouchbaseRepositoryFactory extends ReactiveRepositoryFactor @Override public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { - final ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping - .resolve(metadata.getRepositoryInterface(), metadata.getDomainType()); - - return new ReactiveCouchbaseRepositoryQuery(couchbaseOperations, new QueryMethod(method, metadata, factory)); + final ReactiveCouchbaseOperations couchbaseOperations = couchbaseOperationsMapping.resolve( + metadata.getRepositoryInterface(), metadata.getDomainType()); + return new ReactiveCouchbaseRepositoryQuery(couchbaseOperations, + new CouchbaseQueryMethod(method, metadata, factory, mappingContext), namedQueries); } } diff --git a/src/test/java/org/springframework/data/couchbase/core/query/QueryCriteriaTests.java b/src/test/java/org/springframework/data/couchbase/core/query/QueryCriteriaTests.java index fbfee507..47fac703 100644 --- a/src/test/java/org/springframework/data/couchbase/core/query/QueryCriteriaTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/query/QueryCriteriaTests.java @@ -67,8 +67,8 @@ class QueryCriteriaTests { @Test void testNestedNotIn() { - QueryCriteria c = where("name").is("Bubba").or(where("age").gt(12).or("country").is("Austria")) - .and(where("state").notIn(new String[] { "Alabama", "Florida" })); + QueryCriteria c = where("name").is("Bubba").or(where("age").gt(12).or("country").is("Austria")).and( + where("state").notIn(new String[] { "Alabama", "Florida" })); assertEquals("`name` = \"Bubba\" or (`age` > 12 or `country` = \"Austria\") and " + "(not( (`state` in ( [ \"Alabama\", \"Florida\" ] )) ))", c.export()); } @@ -106,19 +106,28 @@ class QueryCriteriaTests { @Test void testStartingWith() { QueryCriteria c = where("name").startingWith("Cou"); - assertEquals("`name` like \"Cou%\"", c.export()); + assertEquals("`name` like (\"Cou\"||\"%\")", c.export()); } + /* cannot do this properly yet because in arg to when() in + * startingWith() cannot be a QueryCriteria @Test void testStartingWithExpr() { - QueryCriteria c = where("name").startingWith(where("name").plus("")); - assertEquals("`name` like ((\"%\" + ((`name` + \"\"))))", c.export()); + QueryCriteria c = where("name").startingWith(where("name").plus("xxx")); + assertEquals("`name` like (((`name` || "xxx") || ""%""))", c.export()); } + */ @Test void testEndingWith() { QueryCriteria c = where("name").endingWith("ouch"); - assertEquals("`name` like \"%ouch\"", c.export()); + assertEquals("`name` like (\"%\"||\"ouch\")", c.export()); + } + + @Test + void testEndingWithExpr() { + QueryCriteria c = where("name").endingWith(where("name").plus("xxx")); + assertEquals("`name` like (\"%\"||((`name` || \"xxx\")))", c.export()); } @Test @@ -148,7 +157,7 @@ class QueryCriteriaTests { @Test void testNotLike() { QueryCriteria c = where("name").notLike("%Elvis%"); - assertEquals("not( (`name` like \"%Elvis%\") )", c.export()); + assertEquals("not(`name` like \"%Elvis%\")", c.export()); } @Test diff --git a/src/test/java/org/springframework/data/couchbase/domain/Config.java b/src/test/java/org/springframework/data/couchbase/domain/Config.java index 225b2de9..4661b92b 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Config.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Config.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.data.couchbase.domain; import org.springframework.context.annotation.Bean; @@ -23,6 +24,11 @@ import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider; import org.springframework.data.couchbase.repository.auditing.EnableCouchbaseAuditing; import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +/** + * @author Michael Nitschinger + * @author Michael Reiche + * @since 3.0 + */ @Configuration @EnableCouchbaseRepositories @EnableCouchbaseAuditing // this activates auditing @@ -32,23 +38,69 @@ public class Config extends AbstractCouchbaseConfiguration { String password = "password"; String connectionString = "127.0.0.1"; + // if running a clusterAwareIntegrationTests, use those properties + static Class clusterAware = null; + + static { + try { + clusterAware = Class.forName("org.springframework.data.couchbase.util.ClusterAwareIntegrationTests"); + } catch (ClassNotFoundException cnfe) { + } + } + @Override public String getConnectionString() { + if (clusterAware != null) { + try { + if (clusterAware.getMethod("config").invoke(null, (Object[]) null) != null) { + return (String) clusterAware.getMethod("connectionString").invoke(null, (Object[]) null); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } return connectionString; } @Override public String getUserName() { + if (clusterAware != null) { + try { + if (clusterAware.getMethod("config").invoke(null, (Object[]) null) != null) { + return (String) clusterAware.getMethod("username").invoke(null, (Object[]) null); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } return username; } @Override public String getPassword() { + if (clusterAware != null) { + try { + if (clusterAware.getMethod("config").invoke(null, (Object[]) null) != null) { + return (String) clusterAware.getMethod("password").invoke(null, (Object[]) null); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } return password; } @Override public String getBucketName() { + if (clusterAware != null) { + try { + if (clusterAware.getMethod("config").invoke(null, (Object[]) null) != null) { + return (String) clusterAware.getMethod("bucketName").invoke(null, (Object[]) null); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } return bucketname; } diff --git a/src/test/java/org/springframework/data/couchbase/domain/Person.java b/src/test/java/org/springframework/data/couchbase/domain/Person.java index 7943e205..eede7eb5 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Person.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Person.java @@ -30,17 +30,23 @@ public class Person extends AbstractEntity { Optional firstname; Optional lastname; - @CreatedBy private String creator; + @CreatedBy + private String creator; - @LastModifiedBy private String lastModifiedBy; + @LastModifiedBy + private String lastModifiedBy; - @LastModifiedDate private long lastModification; + @LastModifiedDate + private long lastModification; - @CreatedDate private long creationDate; // =System.currentTimeMillis(); + @CreatedDate + private long creationDate; // =System.currentTimeMillis(); - @Version private long version; + @Version + private long version; - public Person() {} + public Person() { + } public Person(String firstname, String lastname) { this(); @@ -54,11 +60,13 @@ public class Person extends AbstractEntity { } static String optional(String name, Optional obj) { - if (obj != null) - if (obj.isPresent()) + if (obj != null) { + if (obj.isPresent()) { return (" " + name + ": '" + obj.get() + "'\n"); - else + } else { return " " + name + ": null\n"; + } + } return ""; } @@ -86,6 +94,10 @@ public class Person extends AbstractEntity { this.lastname = lastname; } + public long getVersion() { + return version; + } + public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Person : {\n"); @@ -93,15 +105,31 @@ public class Person extends AbstractEntity { sb.append(optional(", firstname", firstname)); sb.append(optional(", lastname", lastname)); sb.append(", version : " + version); - if (creator != null) + if (creator != null) { sb.append(", creator : " + creator); - if (creationDate != 0) + } + if (creationDate != 0) { sb.append(", creationDate : " + creationDate); - if (lastModifiedBy != null) + } + if (lastModifiedBy != null) { sb.append(", lastModifiedBy : " + lastModifiedBy); - if (lastModification != 0) + } + if (lastModification != 0) { sb.append(", lastModification : " + lastModification); + } sb.append("}"); return sb.toString(); } + + static String optional(String name, String obj) { + if (obj != null) { + if (obj != null /*.isPresent() */) { + return (" " + name + ": '" + obj/*.get()*/ + "'\n"); + } else { + return " " + name + ": null\n"; + } + } + return ""; + } + } diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java b/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java new file mode 100644 index 00000000..a621c7fc --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonRepository.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012-2020 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 + * + * https://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.domain; + +import org.springframework.data.couchbase.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * @author Michael Reiche + */ +public interface PersonRepository extends CrudRepository { + + /* + * These methods are exercised in HomeController of the test spring-boot DemoApplication + */ + + public List findByLastname(String lastname); + + @Query("#{#n1ql.selectEntity} where firstname = 'Reba' and lastname = $last") + public List any(@Param("last") String any); + + @Query("#{#n1ql.selectEntity} where firstname = 'Reba' and lastname = 'McIntyre'") + public List none(); + + @Query("#{#n1ql.selectEntity} where firstname = 'Reba' and lastname = $1") + public List one(@Param("last") String any); + + @Query("#{#n1ql.selectEntity} where lastname = $2 and firstname = $1") + public List two(@Param("one") String one, @Param("two") String two); + + @Query("#{#n1ql.selectEntity} where lastname in ( $1 )") + public List lastnameIn(@Param("lastnames") String[] lastnames); + + public Person findByFirstname(String firstname); + + public Person findFromReplicasByFirstname(String firstname); + + public List findFromReplicasById(String id); + + public List findByFirstnameLike(String firstname); + + public List findByFirstnameIsNull(); + + public List findByFirstnameIsNotNull(); + + public List findByFirstnameNotLike(String firstname); + + public List findByFirstnameStartingWith(String firstname); + + public List findByFirstnameEndingWith(String firstname); + + public List findByFirstnameContaining(String firstname); + + public List findByFirstnameNotContaining(String firstname); + + public List findByFirstnameBetween(String firstname1, String firstname2); + + public List findByFirstnameIn(String... firstnames); + + public List findByFirstnameNotIn(String... firstnames); + + public List findByFirstnameTrue(Object... o); + + public List findByFirstnameFalse(Object... o); + + List findByFirstnameAndLastname(String firstname, String lastname); + + List findByFirstnameOrLastname(String firstname, String lastname); + + S save(S var1); + + Iterable saveAll(Iterable var1); + + Optional findById(UUID var1); + + boolean existsById(UUID var1); + + Iterable findAll(); + + long count(); + + void deleteById(UUID var1); + + void delete(Person var1); + + void deleteAll(Iterable var1); + + void deleteAll(); + +} \ No newline at end of file diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java index e8888f0c..8d37d5ee 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/UserRepository.java @@ -18,7 +18,9 @@ package org.springframework.data.couchbase.domain; import java.util.List; +import org.springframework.data.couchbase.repository.Query; import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; /** @@ -34,4 +36,9 @@ public interface UserRepository extends PagingAndSortingRepository List findByFirstnameAndLastname(String firstname, String lastname); + @Query("#{#n1ql.selectEntity} where firstname = $1 and lastname = $2") + List getByFirstnameAndLastname(String firstname, String lastname); + + @Query("#{#n1ql.selectEntity} where (firstname = $first or lastname = $last)") + List getByFirstnameOrLastname(@Param("first")String firstname, @Param("last")String lastname); } diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTests.java index bd5c2e5e..b3b62b9e 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/N1qlQueryCreatorTests.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.couchbase.repository.query; import static org.junit.jupiter.api.Assertions.*; @@ -32,12 +31,13 @@ import org.springframework.data.couchbase.core.query.Query; import org.springframework.data.couchbase.domain.User; import org.springframework.data.couchbase.domain.UserRepository; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.repository.query.DefaultParameters; -import org.springframework.data.repository.query.ParameterAccessor; -import org.springframework.data.repository.query.Parameters; -import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.*; import org.springframework.data.repository.query.parser.PartTree; +/** + * @author Michael Nitschinger + * @author Michael Reiche + */ class N1qlQueryCreatorTests { MappingContext, CouchbasePersistentProperty> context; @@ -55,7 +55,8 @@ class N1qlQueryCreatorTests { PartTree tree = new PartTree(input, User.class); Method method = UserRepository.class.getMethod(input, String.class); - N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), "Oliver"), context); + N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), "Oliver"), null, + converter); Query query = creator.createQuery(); assertEquals(query.export(), " WHERE " + where("firstname").is("Oliver").export()); @@ -66,8 +67,8 @@ class N1qlQueryCreatorTests { String input = "findByFirstnameAndLastname"; PartTree tree = new PartTree(input, User.class); Method method = UserRepository.class.getMethod(input, String.class, String.class); - - N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), "John", "Doe"), context); + N1qlQueryCreator creator = new N1qlQueryCreator(tree, getAccessor(getParameters(method), "John", "Doe"), null, + converter); Query query = creator.createQuery(); assertEquals(query.export(), " WHERE " + where("firstname").is("John").and("lastname").is("Doe").export()); diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java new file mode 100644 index 00000000..ee952157 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java @@ -0,0 +1,182 @@ +/* + * Copyright 2017-2019 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 + * + * https://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 org.junit.jupiter.api.Assertions.*; +import static org.springframework.data.couchbase.config.BeanNames.COUCHBASE_TEMPLATE; + +import java.lang.reflect.Method; +import java.util.Properties; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.domain.Config; +import org.springframework.data.couchbase.domain.User; +import org.springframework.data.couchbase.domain.UserRepository; +import org.springframework.data.couchbase.util.Capabilities; +import org.springframework.data.couchbase.util.ClusterType; +import org.springframework.data.couchbase.util.IgnoreWhen; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; +import org.springframework.data.repository.query.*; + +import javax.el.MethodNotFoundException; + +/** + * @author Michael Nitschinger + * @author Michael Reiche + */ +class StringN1qlQueryCreatorTests { + + MappingContext, CouchbasePersistentProperty> context; + CouchbaseConverter converter; + CouchbaseTemplate couchbaseTemplate; + static NamedQueries namedQueries = new PropertiesBasedNamedQueries(new Properties()); + + @BeforeEach + public void beforeEach() { + context = new CouchbaseMappingContext(); + converter = new MappingCouchbaseConverter(context); + ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class); + couchbaseTemplate = (CouchbaseTemplate) ac.getBean(COUCHBASE_TEMPLATE); + } + + @Test + void createsQueryCorrectly() throws Exception { + String input = "getByFirstnameAndLastname"; + Method method = UserRepository.class.getMethod(input, String.class, String.class); + + CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, + new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(), + converter.getMappingContext()); + + StringN1qlQueryCreator creator = new StringN1qlQueryCreator( + getAccessor(getParameters(method), "Oliver", "Twist"), queryMethod, converter, "travel-sample", + QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + + Query query = creator.createQuery(); + assertEquals( + "SELECT META(`travel-sample`).id AS __id, META(`travel-sample`).cas AS __cas, `travel-sample`.* FROM `travel-sample` where firstname = $1 and lastname = $2 AND `_class` = \"org.springframework.data.couchbase.domain.User\"", + query.toN1qlString(couchbaseTemplate.reactive(), User.class, false)); + } + + @Test + void createsQueryCorrectly2() throws Exception { + String input = "getByFirstnameOrLastname"; + Method method = UserRepository.class.getMethod(input, String.class, String.class); + + CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, + new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(), + converter.getMappingContext()); + + StringN1qlQueryCreator creator = new StringN1qlQueryCreator( + getAccessor(getParameters(method), "Oliver", "Twist"), queryMethod, converter, "travel-sample", + QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + + Query query = creator.createQuery(); + assertEquals( + "SELECT META(`travel-sample`).id AS __id, META(`travel-sample`).cas AS __cas, `travel-sample`.* FROM `travel-sample` where (firstname = $first or lastname = $last) AND `_class` = \"org.springframework.data.couchbase.domain.User\"", + query.toN1qlString(couchbaseTemplate.reactive(), User.class, false)); + } + + @Test + void wrongNumberArgs() throws Exception { + String input = "getByFirstnameOrLastname"; + Method method = UserRepository.class.getMethod(input, String.class, String.class); + + CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, + new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(), + converter.getMappingContext()); + + try { + StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"), + queryMethod, converter, "travel-sample", QueryMethodEvaluationContextProvider.DEFAULT, + namedQueries); + } catch (IllegalArgumentException e) { + return; + } + fail("should have failed with IllegalArgumentException: Invalid number of parameters given!"); + } + + @Test + void doesNotHaveAnnotation() throws Exception { + String input = "findByFirstname"; + Method method = UserRepository.class.getMethod(input, String.class); + CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, + new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(), + converter.getMappingContext()); + + try { + StringN1qlQueryCreator creator = new StringN1qlQueryCreator(getAccessor(getParameters(method), "Oliver"), + queryMethod, converter, "travel-sample", QueryMethodEvaluationContextProvider.DEFAULT, + namedQueries); + } catch (IllegalArgumentException e) { + return; + } + fail("should have failed with IllegalArgumentException: query has no inline Query or named Query not found"); + } + + @Test + @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) + void findUsingStringNq1l() throws Exception { + User user = new User(UUID.randomUUID().toString(), "Oliver", "Twist"); + User modified = couchbaseTemplate.upsertById(User.class).one(user); + + String input = "getByFirstnameOrLastname"; + Method method = UserRepository.class.getMethod(input, String.class, String.class); + + CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, + new DefaultRepositoryMetadata(UserRepository.class), new SpelAwareProxyProjectionFactory(), + converter.getMappingContext()); + + StringN1qlQueryCreator creator = new StringN1qlQueryCreator( + getAccessor(getParameters(method), "Oliver", "Twist"), queryMethod, converter, "travel-sample", + QueryMethodEvaluationContextProvider.DEFAULT, namedQueries); + + Query query = creator.createQuery(); + + ExecutableFindByQueryOperation.ExecutableFindByQuery q = (ExecutableFindByQueryOperation.ExecutableFindByQuery) couchbaseTemplate.findByQuery( + User.class).matching(query); + + User u = (User) q.oneValue(); + assertEquals(user, u); + + couchbaseTemplate.removeById().one(user.getId()); + } + + private ParameterAccessor getAccessor(Parameters params, Object... values) { + return new ParametersParameterAccessor(params, values); + } + + private Parameters getParameters(Method method) { + return new DefaultParameters(method); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java index 1f9161a3..5d8bfb96 100644 --- a/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/util/ClusterAwareIntegrationTests.java @@ -52,6 +52,8 @@ public abstract class ClusterAwareIntegrationTests { return PasswordAuthenticator.create(config().adminUsername(), config().adminPassword()); } + public static String username() { return config().adminUsername(); } + public static String password() { return config().adminPassword(); } public static String bucketName() { return config().bucketname(); } @@ -62,13 +64,33 @@ public abstract class ClusterAwareIntegrationTests { * @return the connection string to connect. */ public static String connectionString() { + /* return seedNodes().stream().map(s -> { if (s.kvPort().isPresent()) { - return s.address() + ":" + s.kvPort().get(); + return s.address() + ":" + s.kvPort().get() + "=" + Services.KV; + } else if (s.clusterManagerPort().isPresent()) { + return s.address() + ":" + s.clusterManagerPort().get() + "=" + Services.MANAGER; } else { - return s.address(); + return s.address() ; } }).collect(Collectors.joining(",")); + */ + StringBuffer sb = new StringBuffer(); + for(SeedNode s:seedNodes()) { + if (s.kvPort().isPresent()) { + if(sb.length() > 0 ) sb.append(","); + sb.append (s.address() + ":" + s.kvPort().get() + "=" + Services.KV); + } + if (s.clusterManagerPort().isPresent()) { + if (sb.length() > 0) + sb.append(","); + sb.append(s.address() + ":" + s.clusterManagerPort().get() + "=" + Services.MANAGER); + } + if(sb.length() == 0 ){ + sb.append(s.address()); + } + } + return sb.toString(); } public static Set seedNodes() {