diff --git a/pom.xml b/pom.xml index 6aac4a641..cda7dd3c3 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,9 @@ 3.2.5 2.7.0-SNAPSHOT spring.data.couchbase + 1.1.3 + 5.0.0 + 3.7.4 @@ -37,6 +40,13 @@ + + + com.querydsl + querydsl-apt + ${querydsl} + + org.springframework spring-context-support @@ -160,9 +170,10 @@ javax.annotation javax.annotation-api ${javax-annotation-api} - test + + org.apache.openwebbeans openwebbeans-se @@ -298,6 +309,48 @@ org.asciidoctor asciidoctor-maven-plugin + + com.mysema.maven + apt-maven-plugin + ${apt} + + + com.querydsl + querydsl-apt + ${querydsl} + + + + + generate-test-sources + + test-process + + + target/generated-test-sources + org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + generate-test-sources + + add-source + + + + target/generated-test-sources + + + + + diff --git a/src/main/java/com/querydsl/couchbase/document/AbstractCouchbaseQueryDSL.java b/src/main/java/com/querydsl/couchbase/document/AbstractCouchbaseQueryDSL.java new file mode 100644 index 000000000..66b6744fc --- /dev/null +++ b/src/main/java/com/querydsl/couchbase/document/AbstractCouchbaseQueryDSL.java @@ -0,0 +1,215 @@ +/* + * Copyright 2012-2022 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 com.querydsl.couchbase.document; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.jetbrains.annotations.Nullable; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition; + +import com.querydsl.core.DefaultQueryMetadata; +import com.querydsl.core.JoinExpression; +import com.querydsl.core.QueryMetadata; +import com.querydsl.core.QueryModifiers; +import com.querydsl.core.SimpleQuery; +import com.querydsl.core.support.QueryMixin; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.ExpressionUtils; +import com.querydsl.core.types.FactoryExpression; +import com.querydsl.core.types.Operation; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.ParamExpression; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.Predicate; + +/** + * renamed from AbstractCouchbaseQuery to AbstractCouchbaseQueryDSL to avoid confusion with the AbstractCouchbaseQuery + * that is in the package com.querydsl.couchbase + * + * @author Michael Reiche + */ + +public abstract class AbstractCouchbaseQueryDSL> implements SimpleQuery { + private final CouchbaseDocumentSerializer serializer; + private final QueryMixin queryMixin;// = new QueryMixin(this, new DefaultQueryMetadata(), false); + // TODO private ReadPreference readPreference; + + public AbstractCouchbaseQueryDSL(CouchbaseDocumentSerializer serializer) { + this.serializer = serializer; + @SuppressWarnings("unchecked") // Q is this plus subclass + Q query = (Q) this; + this.queryMixin = new QueryMixin(query, new DefaultQueryMetadata(), false); + } + + /** + * mongodb uses createQuery(Predicate filter) where the serializer creates the 'query'
+ * and then uses the result to create a BasicQuery with queryObject = result
+ * Couchbase Query has a 'criteria' which is a
+ * List criteria
+ * so we could create a List<QueryCriteriaDefinition> or an uber QueryCriteria that combines
+ * all the sub QueryDefinitions in the filter. + */ + protected QueryCriteriaDefinition createCriteria(Predicate predicate) { + // mongodb uses createQuery(Predicate filter) where the serializer creates the 'queryObject' of the BasicQuery + return predicate != null ? (QueryCriteriaDefinition) this.serializer.handle(predicate) : null; + } + + // TODO - need later + // public JoinBuilder join(Path ref, Path target) { + // return new JoinBuilder(this.queryMixin, ref, target); + // } + + // public JoinBuilder join(CollectionPathBase ref, Path target) { + // return new JoinBuilder(this.queryMixin, ref, target); + // } + + // public AnyEmbeddedBuilder anyEmbedded(Path> collection, Path target) { + // return new AnyEmbeddedBuilder(this.queryMixin, collection); + // } + + @Nullable + protected Predicate createFilter(QueryMetadata metadata) { + Predicate filter; + if (!metadata.getJoins().isEmpty()) { + filter = ExpressionUtils.allOf(new Predicate[] { metadata.getWhere(), this.createJoinFilter(metadata) }); + } else { + filter = metadata.getWhere(); + } + return filter; + } + + @Nullable + protected Predicate createJoinFilter(QueryMetadata metadata) { + Map, Predicate> predicates = new HashMap(); + List joins = metadata.getJoins(); + + for (int i = joins.size() - 1; i >= 0; --i) { + JoinExpression join = (JoinExpression) joins.get(i); + Path source = (Path) ((Operation) join.getTarget()).getArg(0); + Path target = (Path) ((Operation) join.getTarget()).getArg(1); + Predicate extraFilters = (Predicate) predicates.get(target.getRoot()); + Predicate filter = ExpressionUtils.allOf(new Predicate[] { join.getCondition(), extraFilters }); + List ids = this.getIds(target.getType(), filter); + if (ids.isEmpty()) { + throw new AbstractCouchbaseQueryDSL.NoResults(); + } + + Path path = ExpressionUtils.path(String.class, source, "$id"); + predicates.merge(source.getRoot(), + ExpressionUtils.in(path, (Collection) ids/* TODO was just ids without casting to Collection */), + ExpressionUtils::and); + } + + Path source = (Path) ((Operation) ((JoinExpression) joins.get(0)).getTarget()).getArg(0); + return predicates.get(source.getRoot()); + } + + private Predicate allOf(Collection predicates) { + return predicates != null ? ExpressionUtils.allOf(predicates) : null; + } + + protected abstract List getIds(Class var1, Predicate var2); + + public Q distinct() { + return this.queryMixin.distinct(); + } + + public Q where(Predicate e) { + return this.queryMixin.where(e); + } + + public Q where(Predicate... e) { + return this.queryMixin.where(e); + } + + public Q limit(long limit) { + return this.queryMixin.limit(limit); + } + + public Q offset(long offset) { + return this.queryMixin.offset(offset); + } + + public Q restrict(QueryModifiers modifiers) { + return this.queryMixin.restrict(modifiers); + } + + public Q orderBy(OrderSpecifier o) { + return this.queryMixin.orderBy(o); + } + + public Q orderBy(OrderSpecifier... o) { + return this.queryMixin.orderBy(o); + } + + public Q set(ParamExpression param, T value) { + return this.queryMixin.set(param, value); + } + + protected Map createProjection(Expression projection) { + if (projection instanceof FactoryExpression) { + Map obj = new HashMap(); + Iterator var3 = ((FactoryExpression) projection).getArgs().iterator(); + + while (var3.hasNext()) { + Object expr = var3.next(); + if (expr instanceof Expression) { + obj.put(expr.toString(), (String) this.serializer.handle((Expression) expr)); + } + } + return obj; + } else { + return null; + } + } + + protected CouchbaseDocument createQuery(@Nullable Predicate predicate) { + return predicate != null ? (CouchbaseDocument) this.serializer.handle(predicate) : new CouchbaseDocument(); + } + + // public void setReadPreference(ReadPreference readPreference) { + // this.readPreference = readPreference; + // } + + protected QueryMixin getQueryMixin() { + return this.queryMixin; + } + + protected CouchbaseDocumentSerializer getSerializer() { + return this.serializer; + } + + // protected ReadPreference getReadPreference() { + // return this.readPreference; + // } + + public CouchbaseDocument asDocument() { + return this.createQuery(this.queryMixin.getMetadata().getWhere()); + } + + public String toString() { + return this.asDocument().toString(); + } + + static class NoResults extends RuntimeException { + NoResults() {} + } +} diff --git a/src/main/java/com/querydsl/couchbase/document/CouchbaseDocumentSerializer.java b/src/main/java/com/querydsl/couchbase/document/CouchbaseDocumentSerializer.java new file mode 100644 index 000000000..c540ec03c --- /dev/null +++ b/src/main/java/com/querydsl/couchbase/document/CouchbaseDocumentSerializer.java @@ -0,0 +1,409 @@ +/* + * Copyright 2012-2022 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 com.querydsl.couchbase.document; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + +import org.springframework.data.couchbase.core.query.QueryCriteria; +import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition; +import org.springframework.data.couchbase.repository.support.DBRef; +import org.springframework.data.domain.Sort; + +import com.querydsl.core.types.Constant; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.ExpressionUtils; +import com.querydsl.core.types.FactoryExpression; +import com.querydsl.core.types.Operation; +import com.querydsl.core.types.Operator; +import com.querydsl.core.types.Ops; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.ParamExpression; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.PathMetadata; +import com.querydsl.core.types.PathType; +import com.querydsl.core.types.SubQueryExpression; +import com.querydsl.core.types.TemplateExpression; +import com.querydsl.core.types.Visitor; + +/** + * Serializes the given Querydsl query to a Document query for Couchbase. + * + * @author Michael Reiche + */ +public abstract class CouchbaseDocumentSerializer implements Visitor { + + boolean workInProgress = true; + + public Object handle(Expression expression) { + return expression.accept(this, null); + } + + public Sort toSort(List> orderBys) { + Sort sort = Sort.unsorted(); + for (OrderSpecifier orderBy : orderBys) { + Object key = orderBy.getTarget().accept(this, null); + // sort.and(Sort.by(orderBy)); + // sort.append(key.toString(), orderBy.getOrder() == Order.ASC ? 1 : -1); + } + return sort; + } + + @Override + public Object visit(Constant expr, Void context) { + if (Enum.class.isAssignableFrom(expr.getType())) { + @SuppressWarnings("unchecked") // Guarded by previous check + Constant> expectedExpr = (Constant>) expr; + return expectedExpr.getConstant().name(); + } else { + return expr.getConstant(); + } + } + + @Override + public Object visit(TemplateExpression expr, Void context) { + throw new UnsupportedOperationException(); + } + + @Override + public Object visit(FactoryExpression expr, Void context) { + throw new UnsupportedOperationException(); + } + + protected String asDBKey(Operation expr, int index) { + return (String) asDBValue(expr, index); + } + + protected Object asDBValue(Operation expr, int index) { + return expr.getArg(index).accept(this, null); + } + + private String regexValue(Operation expr, int index) { + return Pattern.quote(expr.getArg(index).accept(this, null).toString()); + } + + protected QueryCriteriaDefinition asDocument(String key, Object value) { + QueryCriteria qc = null; + if (1 == 1) { + throw new UnsupportedOperationException("Wrong path to create this criteria " + key); + } + if (key.equals("$and") || key.equals("$or") /* value instanceof QueryCriteria[] */) { + throw new UnsupportedOperationException("Wrong path to create this criteria " + key); + } else if (key.equals("$in") /* value instanceof QueryCriteria[] */) { + throw new RuntimeException(("not supported")); + } else { + qc = QueryCriteria.where(key).is(value); + } + + return qc; + } + + @SuppressWarnings("unchecked") + @Override + public Object visit(Operation expr, Void context) { + Operator op = expr.getOperator(); + if (op == Ops.EQ) { + if (expr.getArg(0) instanceof Operation) { + Operation lhs = (Operation) expr.getArg(0); + if (lhs.getOperator() == Ops.COL_SIZE || lhs.getOperator() == Ops.ARRAY_SIZE + || lhs.getOperator() == Ops.STRING_LENGTH) { + // return asDocument(asDBKey(lhs, 0), asDocument("$size", asDBValue(expr, 1))); + return QueryCriteria.where(asDBKey(expr, 0)).is(asDBValue(expr, 1)); + } else { + throw new UnsupportedOperationException("Illegal operation " + expr); + } + } else if (expr.getArg(0) instanceof Path) { + /* + Path path = (Path) expr.getArg(0); + Constant constant = (Constant) expr.getArg(1); + return asDocument(asDBKey(expr, 0), convert(path, constant)); + */ + return QueryCriteria.where(asDBKey(expr, 0)).is(asDBValue(expr, 1)); + } + } else if (op == Ops.STRING_IS_EMPTY) { + // return asDocument(asDBKey(expr, 0), ""); + return QueryCriteria.where(asDBKey(expr, 0)).isNotValued().or(asDBKey(expr, 0)).is(""); + } else if (op == Ops.NOT) { + // Handle the not's child + Operation subOperation = (Operation) expr.getArg(0); + Operator subOp = subOperation.getOperator(); + if (subOp == Ops.IN) { + return visit( + ExpressionUtils.operation(Boolean.class, Ops.NOT_IN, subOperation.getArg(0), subOperation.getArg(1)), + context); + } else { + QueryCriteria arg = (QueryCriteria) handle(expr.getArg(0)); + return arg.negate(); // negate(arg); + } + + } else if (op == Ops.AND) { + // return asDocument("$and", collectConnectorArgs("$and", expr)); + return collectConnectorArgs("$and", expr); + } else if (op == Ops.OR) { + // return asDocument("$or", collectConnectorArgs("$or", expr)); + return collectConnectorArgs("$or", expr); + } else if (op == Ops.NE) { + // Path path = (Path) expr.getArg(0); + // Constant constant = (Constant) expr.getArg(1); + // return asDocument(asDBKey(expr, 0), asDocument("$ne", convert(path, constant))); + return QueryCriteria.where(asDBKey(expr, 0)).ne(asDBValue(expr, 1)); + } else if (op == Ops.STARTS_WITH) { + // return asDocument(asDBKey(expr, 0), new CBRegularExpression("^" + regexValue(expr, 1))); + return QueryCriteria.where(asDBKey(expr, 0)).startingWith(asDBValue(expr, 1)); + } else if (op == Ops.STARTS_WITH_IC) { + // return asDocument(asDBKey(expr, 0), new CBRegularExpression("^" + regexValue(expr, 1), "i")); + return QueryCriteria.where(asDBKey(expr, 0)).upper() + .startingWith(asDBValue(expr, 1).toString().toUpperCase(Locale.ROOT)); + } else if (op == Ops.ENDS_WITH) { + // return asDocument(asDBKey(expr, 0), new CBRegularExpression(regexValue(expr, 1) + "$")); + return QueryCriteria.where(asDBKey(expr, 0)).endingWith(asDBValue(expr, 1)); + } else if (op == Ops.ENDS_WITH_IC) { + // return asDocument(asDBKey(expr, 0), new CBRegularExpression(regexValue(expr, 1) + "$", "i")); + return QueryCriteria.where(asDBKey(expr, 0)).upper() + .endingWith(asDBValue(expr, 1).toString().toUpperCase(Locale.ROOT)); + } else if (op == Ops.EQ_IGNORE_CASE) { + // return asDocument(asDBKey(expr, 0), new CBRegularExpression("^" + regexValue(expr, 1) + "$", "i")); + return QueryCriteria.where(asDBKey(expr, 0)).upper().eq(asDBValue(expr, 1).toString().toUpperCase(Locale.ROOT)); + } else if (op == Ops.STRING_CONTAINS) { + // return asDocument(asDBKey(expr, 0), new CBRegularExpression(".*" + regexValue(expr, 1) + ".*")); + return QueryCriteria.where(asDBKey(expr, 0)).containing(asDBValue(expr, 1)); + } else if (op == Ops.STRING_CONTAINS_IC) { + // return asDocument(asDBKey(expr, 0), new CBRegularExpression(".*" + regexValue(expr, 1) + ".*", "i")); + return QueryCriteria.where(asDBKey(expr, 0)).upper() + .containing(asDBValue(expr, 1).toString().toUpperCase(Locale.ROOT)); + /* + } else if (op == Ops.MATCHES) { + //return asDocument(asDBKey(expr, 0), new CBRegularExpression(asDBValue(expr, 1).toString())); + return QueryCriteria.where(asDBKey(expr, 0)).like(asDBValue(expr,1)); + } else if (op == Ops.MATCHES_IC) { + //return asDocument(asDBKey(expr, 0), new CBRegularExpression(asDBValue(expr, 1).toString(), "i")); + return QueryCriteria.where("UPPER("+asDBKey(expr, 0)+")").like("UPPER("+asDBValue(expr,1)+")"); + */ + } else if (op == Ops.LIKE) { + // String regex = ExpressionUtils.likeToRegex((Expression) expr.getArg(1)).toString(); + // return asDocument(asDBKey(expr, 0), new CBRegularExpression(regex)); + return QueryCriteria.where(asDBKey(expr, 0)).like(asDBValue(expr, 1)); + } else if (op == Ops.LIKE_IC) { + // String regex = ExpressionUtils.likeToRegex((Expression) expr.getArg(1)).toString(); + // return asDocument(asDBKey(expr, 0), new CBRegularExpression(regex, "i")); + return QueryCriteria.where(asDBKey(expr, 0)).upper().like(asDBValue(expr, 1).toString().toUpperCase(Locale.ROOT)); + } else if (op == Ops.BETWEEN) { + // Document value = new Document("$gte", this.asDBValue(expr, 1)); + // value.append("$lte", this.asDBValue(expr, 2)); + // return this.asDocument(this.asDBKey(expr, 0), value); + return QueryCriteria.where(asDBKey(expr, 0)).between(asDBValue(expr, 1), asDBValue(expr, 2)); + } else if (op == Ops.IN) { + int constIndex = 0; + int exprIndex = 1; + if (expr.getArg(1) instanceof Constant) { + constIndex = 1; + exprIndex = 0; + } + if (Collection.class.isAssignableFrom(expr.getArg(constIndex).getType())) { + @SuppressWarnings("unchecked") // guarded by previous check + Collection values = ((Constant>) expr.getArg(constIndex)).getConstant(); + // return asDocument(asDBKey(expr, exprIndex), asDocument("$in", values)); + return QueryCriteria.where(asDBKey(expr, exprIndex)).in(values); + } else { // I think framework already converts IN to EQ if arg is not a collection + // Path path = (Path) expr.getArg(exprIndex); + // Constant constant = (Constant) expr.getArg(constIndex); + // return asDocument(asDBKey(expr, exprIndex), convert(path, constant)); + Object value = expr.getArg(constIndex); + return QueryCriteria.where(asDBKey(expr, exprIndex)).eq(value); + } + + } else if (op == Ops.NOT_IN) { + int constIndex = 0; + int exprIndex = 1; + if (expr.getArg(1) instanceof Constant) { + constIndex = 1; + exprIndex = 0; + } + if (Collection.class.isAssignableFrom(expr.getArg(constIndex).getType())) { + @SuppressWarnings("unchecked") // guarded by previous check + Collection values = ((Constant>) expr.getArg(constIndex)).getConstant(); + // return asDocument(asDBKey(expr, exprIndex), asDocument("$nin", values)); + return QueryCriteria.where(asDBKey(expr, exprIndex)).notIn(values); + } else { // I think framework already converts NOT_IN to NE if arg is not a collection + // Path path = (Path) expr.getArg(exprIndex); + // Constant constant = (Constant) expr.getArg(constIndex); + // return asDocument(asDBKey(expr, exprIndex), asDocument("$ne", convert(path, constant))); + Object value = expr.getArg(constIndex); + return QueryCriteria.where(asDBKey(expr, exprIndex)).ne(value); + } + + } else if (op == Ops.COL_IS_EMPTY) { + // List list = new ArrayList(2); + // list.add(asDocument(asDBKey(expr, 0), new ArrayList())); + // list.add(asDocument(asDBKey(expr, 0), asDocument("$exists", false))); + // return asDocument("$or", list); + return QueryCriteria.where(asDBKey(expr, 0)).isNotValued(); + } else if (op == Ops.LT) { + // return asDocument(asDBKey(expr, 0), asDocument("$lt", asDBValue(expr, 1))); + return QueryCriteria.where(asDBKey(expr, 0)).lt(asDBValue(expr, 1)); + } else if (op == Ops.GT) { + // return asDocument(asDBKey(expr, 0), asDocument("$gt", asDBValue(expr, 1))); + return QueryCriteria.where(asDBKey(expr, 0)).gt(asDBValue(expr, 1)); + } else if (op == Ops.LOE) { + // return asDocument(asDBKey(expr, 0), asDocument("$lte", asDBValue(expr, 1))); + return QueryCriteria.where(asDBKey(expr, 0)).lte(asDBValue(expr, 1)); + } else if (op == Ops.GOE) { + // return asDocument(asDBKey(expr, 0), asDocument("$gte", asDBValue(expr, 1))); + return QueryCriteria.where(asDBKey(expr, 0)).gte(asDBValue(expr, 1)); + } else if (op == Ops.IS_NULL) { + // return asDocument(asDBKey(expr, 0), asDocument("$exists", false)); + return QueryCriteria.where(asDBKey(expr, 0)).isNull(); + } else if (op == Ops.IS_NOT_NULL) { + // return asDocument(asDBKey(expr, 0), asDocument("$exists", true)); + return QueryCriteria.where(asDBKey(expr, 0)).isNotNull(); + } else if (op == Ops.CONTAINS_KEY) { // TODO not sure about this one + Path path = (Path) expr.getArg(0); + // Expression key = expr.getArg(1); + // return asDocument(visit(path, context) + "." + key.toString(), asDocument("$exists", true)); + return QueryCriteria.where("meta().id"/*asDBKey(expr, 0)*/).eq(asDBKey(expr, 1)); + } else if (op == Ops.STRING_LENGTH) { + return "LENGTH(" + asDBKey(expr, 0) + ")";// QueryCriteria.where(asDBKey(expr, 0)).size(); + } + + throw new UnsupportedOperationException("Illegal operation " + expr); + } + + /* TODO -- need later + private Object negate(QueryCriteriaDefinition arg) { + List list = new ArrayList(); + for (Map.Entry entry : arg.entrySet()) { + if (entry.getKey().equals("$or")) { + list.add(asDocument("$nor", entry.getValue())); + + } else if (entry.getKey().equals("$and")) { + List list2 = new ArrayList(); + for (Object o : ((Collection) entry.getValue())) { + list2.add(negate((QueryCriteriaDefinition) o)); + } + list.add(asDocument("$or", list2)); + + } else if (entry.getValue() instanceof Pattern || entry.getValue() instanceof CBRegularExpression) { + list.add(asDocument(entry.getKey(), asDocument("$not", entry.getValue()))); + + } else if (entry.getValue() instanceof QueryCriteriaDefinition) { + list.add(negate(entry.getKey(), (QueryCriteriaDefinition) entry.getValue())); + + } else { + list.add(asDocument(entry.getKey(), asDocument("$ne", entry.getValue()))); + } + } + return list.size() == 1 ? list.get(0) : asDocument("$or", list); + } + + private Object negate(String key, QueryCriteriaDefinition value) { + if (value.size() == 1) { + return asDocument(key, asDocument("$not", value)); + } else { + List list2 = new ArrayList(); + for (Map.Entry entry2 : value.entrySet()) { + list2.add(asDocument(key, asDocument("$not", asDocument(entry2.getKey(), entry2.getValue())))); + } + return asDocument("$or", list2); + } + } + */ + + /* TODO -- need later + protected Object convert(Path property, Constant constant) { + if (isReference(property)) { + return asReference(constant.getConstant()); + } else if (isId(property)) { + if (isReference(property.getMetadata().getParent())) { + return asReferenceKey(property.getMetadata().getParent().getType(), constant.getConstant()); + } else if (constant.getType().equals(String.class) && isImplicitObjectIdConversion()) { + String id = (String) constant.getConstant(); + return ObjectId.isValid(id) ? new ObjectId(id) : id; + } + } + return visit(constant, null); + } + */ + + protected boolean isImplicitObjectIdConversion() { + return true; + } + + protected DBRef asReferenceKey(Class entity, Object id) { + // TODO override in subclass + throw new UnsupportedOperationException(); + } + + protected abstract DBRef asReference(Object constant); + + protected abstract boolean isReference(Path arg); + + protected boolean isId(Path arg) { + // TODO override in subclass + return false; + } + + @Override + public String visit(Path expr, Void context) { + PathMetadata metadata = expr.getMetadata(); + if (metadata.getParent() != null) { + Path parent = metadata.getParent(); + if (parent.getMetadata().getPathType() == PathType.DELEGATE) { + parent = parent.getMetadata().getParent(); + } + if (metadata.getPathType() == PathType.COLLECTION_ANY) { + return visit(parent, context); + } else if (parent.getMetadata().getPathType() != PathType.VARIABLE) { + String rv = getKeyForPath(expr, metadata); + String parentStr = visit(parent, context); + return rv != null ? parentStr + "." + rv : parentStr; + } + } + return getKeyForPath(expr, metadata); + } + + protected String getKeyForPath(Path expr, PathMetadata metadata) { + return metadata.getElement().toString(); + } + + @Override + public Object visit(SubQueryExpression expr, Void context) { + throw new UnsupportedOperationException(); + } + + @Override + public Object visit(ParamExpression expr, Void context) { + throw new UnsupportedOperationException(); + } + + private QueryCriteriaDefinition collectConnectorArgs(String operator, Operation operation) { + QueryCriteria first = null; + for (Expression exp : operation.getArgs()) { + QueryCriteria document = (QueryCriteria) handle(exp); + if (first == null) { + first = document; + } else { + if (operator.equals("$or")) { + first = first.or(document); + } else if (operator.equals("$and")) { + first = first.and(document); + } + } + } + return first; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java index e216779ff..b5231ef19 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java @@ -105,6 +105,25 @@ public Object convertForWriteIfNeeded(Object value) { } + /* TODO needed later + @Override + public Object convertToCouchbaseType(Object value, TypeInformation typeInformation) { + if (value == null) { + return null; + } + + return this.conversions.getCustomWriteTarget(value.getClass()) // + .map(it -> (Object) this.conversionService.convert(value, it)) // + .orElseGet(() -> Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value); + + } + + @Override + public Object convertToCouchbaseType(String source) { + return source; + } + */ + @Override public Class getWriteClassFor(Class clazz) { return this.conversions.getCustomWriteTarget(clazz).orElse(clazz); diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java index ee7a68ccf..23e7b20a3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors + * Copyright 2012-2022 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. @@ -29,6 +29,7 @@ * * @author Michael Nitschinger * @author Simon Baslé + * @author Michael Reiche */ public interface CouchbaseConverter extends EntityConverter, CouchbasePersistentProperty, Object, CouchbaseDocument>, @@ -61,4 +62,10 @@ public interface CouchbaseConverter * @return the alias value for the type */ Alias getTypeAlias(TypeInformation info); + + // TODO needed later + // CouchbaseTypeMapper getMapper(); + // Object convertToCouchbaseType(Object source, TypeInformation typeInformation); + // + // Object convertToCouchbaseType(String source); } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java index af9ed2941..cf4a42411 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors + * Copyright 2012-2022 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. @@ -35,6 +35,7 @@ * * @author Michael Nitschinger * @author Mark Paluch + * @author Michael Reiche */ public class BasicCouchbasePersistentEntity extends BasicPersistentEntity implements CouchbasePersistentEntity, EnvironmentAware { @@ -164,4 +165,14 @@ public boolean isTouchOnRead() { return annotation == null ? false : annotation.touchOnRead() && getExpiry() > 0; } + @Override + public boolean hasTextScoreProperty() { + return false; + } + + @Override + public CouchbasePersistentProperty getTextScoreProperty() { + return null; + } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java index f768aab9c..17a53cd3e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java @@ -24,6 +24,7 @@ * Represents an entity that can be persisted which contains 0 or more properties. * * @author Michael Nitschinger + * @author Michael Reiche */ public interface CouchbasePersistentEntity extends PersistentEntity { @@ -61,4 +62,7 @@ public interface CouchbasePersistentEntity extends PersistentEntity getCriteriaList() { + return this.criteria; + } + /** * set the postional parameters on the query object There can only be named parameters or positional parameters - not * both. @@ -209,7 +229,7 @@ public Query scanConsistency(final QueryScanConsistency queryScanConsistency) { * @return */ public Query with(final Sort sort) { - Assert.notNull(sort, "Sort must not be null!"); + notNull(sort, "Sort must not be null!"); if (sort.isUnsorted()) { return this; } @@ -348,9 +368,9 @@ public String toN1qlRemoveString(ReactiveCouchbaseTemplate template, String coll return statement.toString(); } - public static StringBasedN1qlQueryParser.N1qlSpelValues getN1qlSpelValues( - ReactiveCouchbaseTemplate template, String collectionName, - Class domainClass, Class returnClass, boolean isCount, String[] distinctFields, String[] fields) { + public static StringBasedN1qlQueryParser.N1qlSpelValues getN1qlSpelValues(ReactiveCouchbaseTemplate template, + String collectionName, Class domainClass, Class returnClass, boolean isCount, String[] distinctFields, + String[] fields) { String typeKey = template.getConverter().getTypeKey(); final CouchbasePersistentEntity persistentEntity = template.getConverter().getMappingContext() .getRequiredPersistentEntity(domainClass); @@ -391,4 +411,51 @@ public Meta getMeta() { return meta; } + public boolean equals(Object o) { + if (!o.getClass().isAssignableFrom(getClass())) { + return false; + } + Query that = (Query) o; + if (this.criteria.size() != that.criteria.size()) { + return false; + } + if (this.criteria.equals(that.criteria)) { + return false; + } + int i = 0; + for (QueryCriteriaDefinition thisCriteria : this.criteria) { + if (!thisCriteria.equals(that.criteria.get(i))) { + return false; + } + } + + if (this.parameters.equals(that.parameters)) { + return false; + } + ; + if (this.skip != that.skip) { + return false; + } + if (this.limit != that.limit) { + return false; + } + if (this.distinct != that.distinct) { + return false; + } + + if (Arrays.equals(this.distinctFields, that.distinctFields)) { + return false; + } + if (this.sort != that.sort) { + return false; + } + if (this.queryScanConsistency != that.queryScanConsistency) { + return false; + } + if (!meta.equals(that.meta)) { + return false; + } + return true; + } + } 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 319fed4a2..5c6795e59 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 @@ -17,7 +17,6 @@ import static org.springframework.data.couchbase.core.query.N1QLExpression.x; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Formatter; @@ -41,11 +40,11 @@ */ public class QueryCriteria implements QueryCriteriaDefinition { - private final N1QLExpression key; + private N1QLExpression key; /** * Holds the chain itself, the current operator being always the last one. */ - private List criteriaChain; + private LinkedList criteriaChain; /** * Represents how the chain is hung together, null only for the first element. */ @@ -54,29 +53,27 @@ public class QueryCriteria implements QueryCriteriaDefinition { private Object[] value; private String format; - QueryCriteria(List chain, N1QLExpression key, Object[] value, ChainOperator chainOperator) { + public QueryCriteria(LinkedList chain, N1QLExpression key, Object[] value, + ChainOperator chainOperator) { this(chain, key, value, chainOperator, null, null); } - QueryCriteria(List chain, N1QLExpression key, Object value, ChainOperator chainOperator) { + QueryCriteria(LinkedList chain, N1QLExpression key, Object value, ChainOperator chainOperator) { this(chain, key, new Object[] { value }, chainOperator, null, null); } - QueryCriteria(List chain, N1QLExpression key, Object[] value, ChainOperator chainOperator, + QueryCriteria(LinkedList chain, N1QLExpression key, Object[] value, ChainOperator chainOperator, String operator, String format) { - this.criteriaChain = chain; - criteriaChain.add(this); + this.criteriaChain = chain != null ? chain : new LinkedList<>(); + this.chainOperator = chainOperator; + this.criteriaChain.add(this);// add the new one to the chain. The new one has the chainOperator set this.key = key; this.value = value; - this.chainOperator = chainOperator; + // this.chainOperator = chainOperator; ignored now. this.operator = operator; this.format = format; } - Object[] getValue() { - return value; - } - /** * Static factory method to create a Criteria using the provided String key. */ @@ -88,13 +85,56 @@ public static QueryCriteria where(String key) { * Static factory method to create a Criteria using the provided N1QLExpression key. */ public static QueryCriteria where(N1QLExpression key) { - return new QueryCriteria(new ArrayList<>(), key, null, null); + return new QueryCriteria(null, key, null, null); } + // wrap criteria (including the criteriaChain) in a new QueryCriteria and set the queryChain of the original criteria + // to just itself. The new query will be the value[0] of the original query. + private void replaceThisAsWrapperOf(QueryCriteria criteria) { + QueryCriteria qc = new QueryCriteria(criteria.criteriaChain, criteria.key, criteria.value, criteria.chainOperator, + criteria.operator, criteria.format); + criteriaChain.remove(criteria); // we replaced it with the clone + criteriaChain = new LinkedList<>(); + criteriaChain.add(this); + key = N1QLExpression.WRAPPER(); + operator = ""; + value = new Object[] { qc }; + chainOperator = null; + } + + // wrap criteria (including the criteriaChain) in a new QueryCriteria and set the queryChain of the original criteria + // to just itself. The new query will be the value[0] of the original query. + private void wrapThis() { + QueryCriteria qc = new QueryCriteria(this.criteriaChain, this.key, this.value, this.chainOperator, this.operator, + this.format); + this.criteriaChain.remove(this); // we replaced it with the clone + this.criteriaChain = new LinkedList<>(); + this.criteriaChain.add(this); + this.key = N1QLExpression.WRAPPER(); + this.operator = ""; + this.format = null; + this.value = new Object[] { qc }; + this.chainOperator = null; + + } + + // wrap criteria (including the criteriaChain) in a new QueryCriteria and set the queryChain of the original criteria + // to just itself. The new query will be the value[0] of the original query. private static QueryCriteria wrap(QueryCriteria criteria) { - QueryCriteria qc = new QueryCriteria(new LinkedList<>(), criteria.key, criteria.value, null, criteria.operator, - criteria.format); - return qc; + QueryCriteria qc = new QueryCriteria(criteria.criteriaChain, criteria.key, criteria.value, criteria.chainOperator, + criteria.operator, criteria.format); + criteria.criteriaChain.remove(qc); + int idx = criteria.criteriaChain.indexOf(criteria); + criteria.criteriaChain.add(idx, qc); + criteria.criteriaChain.remove(criteria); // we replaced it with the clone + criteria.criteriaChain = new LinkedList<>(); + criteria.criteriaChain.add(criteria); + criteria.key = N1QLExpression.WRAPPER(); + criteria.operator = ""; + criteria.format = null; + criteria.value = new Object[] { qc }; + criteria.chainOperator = null; + return criteria; } public QueryCriteria and(String key) { @@ -102,11 +142,40 @@ public QueryCriteria and(String key) { } public QueryCriteria and(N1QLExpression key) { + // this.criteriaChain.getLast().setChainOperator(ChainOperator.AND); return new QueryCriteria(this.criteriaChain, key, null, ChainOperator.AND); } public QueryCriteria and(QueryCriteria criteria) { - return new QueryCriteria(this.criteriaChain, null, criteria, ChainOperator.AND); + if (this.criteriaChain != null && !this.criteriaChain.contains(this)) { + throw new RuntimeException("criteria chain does not include this"); + } + if (this.criteriaChain == null) { + this.criteriaChain = new LinkedList<>(); + this.criteriaChain.add(this); + } + QueryCriteria newThis = wrap(this); + QueryCriteria qc = wrap(criteria); + newThis.criteriaChain.add(qc); + qc.setChainOperator(ChainOperator.AND); + newThis.chainOperator = ChainOperator.AND;// otherwise we get "A chain operator must be present when chaining" + return newThis; + } + + // upper and lower should work like and/or by pushing "this" to a 'value' and changing "this" to upper()/lower() + public QueryCriteria upper() { + key = x("UPPER(" + key + ")"); + operator = "UPPER"; + value = new Object[] {}; + return this; + } + + // upper and lower should work like and/or by pushing "this" to a 'value' and changing "this" to upper()/lower() + public QueryCriteria lower() { + key = x("LOWER(" + key + ")"); + operator = "LOWER"; + value = new Object[] {}; + return this; } public QueryCriteria or(String key) { @@ -114,11 +183,25 @@ public QueryCriteria or(String key) { } public QueryCriteria or(N1QLExpression key) { + // this.criteriaChain.getLast().setChainOperator(ChainOperator.OR); return new QueryCriteria(this.criteriaChain, key, null, ChainOperator.OR); } public QueryCriteria or(QueryCriteria criteria) { - return new QueryCriteria(this.criteriaChain, null, criteria, ChainOperator.OR); + if (this.criteriaChain != null && !this.criteriaChain.contains(this)) { + throw new RuntimeException("criteria chain does not include this"); + } + if (this.criteriaChain == null) { + this.criteriaChain = new LinkedList<>(); + this.criteriaChain.add(this); + } + QueryCriteria newThis = wrap(this); + QueryCriteria qc = wrap(criteria); + qc.criteriaChain = newThis.criteriaChain; + newThis.criteriaChain.add(qc); + qc.setChainOperator(ChainOperator.OR); + newThis.chainOperator = ChainOperator.OR;// otherwise we get "A chain operator must be present when chaining" + return newThis; } public QueryCriteria eq(@Nullable Object o) { @@ -204,12 +287,28 @@ public QueryCriteria arrayContaining(@Nullable Object o) { } public QueryCriteria notContaining(@Nullable Object o) { - value = new QueryCriteria[] { wrap(containing(o)) }; + replaceThisAsWrapperOf(containing(o)); operator = "NOT"; format = "not( %3$s )"; return this; } + public QueryCriteria negate() { + replaceThisAsWrapperOf(this); + operator = "NOT"; + format = "not( %3$s )"; + // criteriaChain = new LinkedList<>(); + // criteriaChain.add(this); + return this; + } + + public QueryCriteria size() { + replaceThisAsWrapperOf(this); + operator = "LENGTH"; + format = "LENGTH( %3$s )"; + return this; + } + public QueryCriteria like(@Nullable Object o) { operator = "LIKE"; value = new Object[] { o }; @@ -315,10 +414,7 @@ public QueryCriteria in(@Nullable Object... o) { } public QueryCriteria notIn(@Nullable Object... o) { - value = new QueryCriteria[] { wrap(in(o)) }; - operator = "NOT"; - format = "not( %3$s )"; // field = 1$, operator = 2$, value=$3, $4, ... - return this; + return in(o).negate(); } public QueryCriteria TRUE() { // true/false are reserved, use TRUE/FALSE @@ -329,9 +425,9 @@ public QueryCriteria TRUE() { // true/false are reserved, use TRUE/FALSE } public QueryCriteria FALSE() { - value = new QueryCriteria[] { wrap(TRUE()) }; - operator = "not"; - format = "not( %3$s )"; + value = null; + operator = "NOT"; + format = "not(%1$s)"; // field = 1$, operator = 2$, value=$3, $4, ... return this; } @@ -352,7 +448,7 @@ public String export(int[] paramIndexPtr, JsonValue parameters, CouchbaseConvert for (QueryCriteria c : this.criteriaChain) { if (!first) { if (c.chainOperator == null) { - throw new IllegalStateException("A chain operator must be present when chaining!"); + throw new IllegalStateException("A chain operator must be present when chaining! \n" + c); } // the consistent place to output this would be in the c.exportSingle(output) about five lines down output.append(" ").append(c.chainOperator.representation).append(" "); @@ -509,7 +605,12 @@ private String maybeBackTic(String value) { } } - enum ChainOperator { + @Override + public void setChainOperator(ChainOperator chainOperator) { + this.chainOperator = chainOperator; + } + + public enum ChainOperator { AND("and"), OR("or"); private final String representation; @@ -518,9 +619,25 @@ enum ChainOperator { this.representation = representation; } + public static ChainOperator from(String operator) { + return valueOf(operator.substring(1).toUpperCase()); + } + public String getRepresentation() { return representation; } } + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("{key: " + this.key); + sb.append(", operator: " + this.operator); + sb.append(", value: " + this.value); + sb.append(", criteriaChain.size(): " + this.criteriaChain.size() + "\n"); + for (QueryCriteria qc : criteriaChain) { + sb.append(" key: " + qc.key + " operator: " + qc.operator + "\n"); + } + sb.append("}"); + return sb.toString(); + } } 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 c9f68341c..0e670932b 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 @@ -1,5 +1,5 @@ /* - * Copyright 2010-2020 the original author or authors. + * Copyright 2010-2022 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. @@ -15,9 +15,10 @@ */ package org.springframework.data.couchbase.core.query; -import com.couchbase.client.java.json.JsonValue; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import com.couchbase.client.java.json.JsonValue; + /** * @author Oliver Gierke * @author Christoph Strobl @@ -44,4 +45,6 @@ public interface QueryCriteriaDefinition { * @return string containing part of N1QL query */ String export(); + + void setChainOperator(QueryCriteria.ChainOperator chainOperator); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/ResultProcessingConverter.java b/src/main/java/org/springframework/data/couchbase/repository/query/ResultProcessingConverter.java index 64066e026..c95fff75f 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/ResultProcessingConverter.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/ResultProcessingConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2022 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. diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/BasicQuery.java b/src/main/java/org/springframework/data/couchbase/repository/support/BasicQuery.java new file mode 100644 index 000000000..3fd03449f --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/BasicQuery.java @@ -0,0 +1,120 @@ +/* + * Copyright 2012-2022 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.support; + +import static org.springframework.util.ObjectUtils.nullSafeEquals; +import static org.springframework.util.ObjectUtils.nullSafeHashCode; + +import java.util.Map; + +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.query.Query; +import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition; +import org.springframework.data.domain.Sort; +import org.springframework.util.Assert; + +/** + * BasicQuery for Querydsl + * + * @author Michael Reiche + */ +public class BasicQuery extends Query { + + Map projectionFields; + + /** + * Create a new {@link BasicQuery} given a query {@link CouchbaseDocument} and field specification + * {@link CouchbaseDocument}. + * + * @param query must not be {@literal null}. + * @param projectionFields must not be {@literal null}. + * @throws IllegalArgumentException when {@code sortObject} or {@code fieldsObject} is {@literal null}. + */ + public BasicQuery(Query query, Map projectionFields) { + super(query); + Assert.notNull(projectionFields, "Field document must not be null"); + this.projectionFields = projectionFields; + } + + public BasicQuery(QueryCriteriaDefinition criteria, Map projectionFields) { + addCriteria(criteria); + this.projectionFields = projectionFields; + } + + /** + * Set the sort {@link CouchbaseDocument}. + * + * @param sort must not be {@literal null}. + * @throws IllegalArgumentException when {@code sortObject} is {@literal null}. + */ + public void setSort(Sort sort) { + Assert.notNull(sort, "Sort must not be null"); + with(sort); + } + + /* + * indicates if the query is sorted + */ + public boolean isSorted() { + return sort != null && sort != Sort.unsorted(); + } + + /** + * Set the fields (projection) {@link CouchbaseDocument}. + * + * @param projectionFields must not be {@literal null}. + * @throws IllegalArgumentException when {@code fieldsObject} is {@literal null}. + */ + public void setProjectionFields(Map projectionFields) { + Assert.notNull(projectionFields, "Field document must not be null"); + this.projectionFields = projectionFields; + } + + /* + * (non-Javadoc) + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof BasicQuery)) { + return false; + } + BasicQuery that = (BasicQuery) o; + return querySettingsEquals(that) && // + nullSafeEquals(projectionFields, that.projectionFields) && // + nullSafeEquals(sort, that.sort); + } + + private boolean querySettingsEquals(BasicQuery that) { + return super.equals(that); + } + + /* + * (non-Javadoc) + */ + @Override + public int hashCode() { + + int result = super.hashCode(); + result = 31 * result + nullSafeHashCode(getCriteriaList()); + result = 31 * result + nullSafeHashCode(projectionFields); + result = 31 * result + nullSafeHashCode(sort); + + return result; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseAnnotationProcessor.java b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseAnnotationProcessor.java new file mode 100644 index 000000000..059931d98 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/CouchbaseAnnotationProcessor.java @@ -0,0 +1,63 @@ +/* + * Copyright 2011-2022 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.support; + +import java.util.Collections; + +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.tools.Diagnostic; + +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.lang.Nullable; + +import com.querydsl.apt.AbstractQuerydslProcessor; +import com.querydsl.apt.Configuration; +import com.querydsl.apt.DefaultConfiguration; +import com.querydsl.core.annotations.QueryEmbeddable; +import com.querydsl.core.annotations.QueryEmbedded; +import com.querydsl.core.annotations.QueryEntities; +import com.querydsl.core.annotations.QuerySupertype; +import com.querydsl.core.annotations.QueryTransient; + +/** + * Annotation processor to create Querydsl query types for QueryDsl annotated classes. + * + * @author Michael Reiche + */ +@SupportedAnnotationTypes({ "com.querydsl.core.annotations.*", "org.springframework.data.couchbase.core.mapping.*" }) +@SupportedSourceVersion(SourceVersion.RELEASE_6) +public class CouchbaseAnnotationProcessor extends AbstractQuerydslProcessor { + + /* + * (non-Javadoc) + * @see com.querydsl.apt.AbstractQuerydslProcessor#createConfiguration(javax.annotation.processing.RoundEnvironment) + */ + @Override + protected Configuration createConfiguration(@Nullable RoundEnvironment roundEnv) { + + processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Running " + getClass().getSimpleName()); + + DefaultConfiguration configuration = new DefaultConfiguration(processingEnv, roundEnv, Collections.emptySet(), + QueryEntities.class, Document.class, QuerySupertype.class, QueryEmbeddable.class, QueryEmbedded.class, + QueryTransient.class); + configuration.setUnknownAsEmbedded(true); + + return configuration; + } +} 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 d8bff2d00..bc05c5f60 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * Copyright 2013-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,13 @@ package org.springframework.data.couchbase.repository.support; +import static org.springframework.data.querydsl.QuerydslUtils.QUERY_DSL_PRESENT; + import java.io.Serializable; import java.lang.reflect.Method; import java.util.Optional; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; @@ -30,9 +33,11 @@ import org.springframework.data.couchbase.repository.query.StringBasedCouchbaseQuery; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; @@ -53,6 +58,8 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport { private static final SpelExpressionParser SPEL_PARSER = new SpelExpressionParser(); + private final CouchbaseOperations operations; + /** * Holds the reference to the template. */ @@ -76,7 +83,7 @@ public CouchbaseRepositoryFactory(final RepositoryOperationsMapping couchbaseOpe this.couchbaseOperationsMapping = couchbaseOperationsMapping; this.crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor(); mappingContext = this.couchbaseOperationsMapping.getMappingContext(); - + operations = this.couchbaseOperationsMapping.getDefault(); addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor); } @@ -168,4 +175,44 @@ public RepositoryQuery resolveQuery(final Method method, final RepositoryMetadat } } + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryFragments(org.springframework.data.repository.core.RepositoryMetadata) + */ + @Override + protected RepositoryComposition.RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { + return getRepositoryFragments(metadata, operations); + } + + /** + * Creates {@link RepositoryComposition.RepositoryFragments} based on {@link RepositoryMetadata} to add + * Couchbase-specific extensions. Typically adds a {@link QuerydslCouchbasePredicateExecutor} if the repository + * interface uses Querydsl. + *

+ * Can be overridden by subclasses to customize {@link RepositoryComposition.RepositoryFragments}. + * + * @param metadata repository metadata. + * @param operations the Couchbase operations manager. + * @return + */ + protected RepositoryComposition.RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata, + CouchbaseOperations operations) { + + boolean isQueryDslRepository = QUERY_DSL_PRESENT + && QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); + + if (isQueryDslRepository) { + + if (metadata.isReactiveRepository()) { + throw new InvalidDataAccessApiUsageException( + "Cannot combine Querydsl and reactive repository support in a single interface"); + } + + return RepositoryComposition.RepositoryFragments + .just(new QuerydslCouchbasePredicateExecutor<>(getEntityInformation(metadata.getDomainType()), operations)); + } + + return RepositoryComposition.RepositoryFragments.empty(); + } + } diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/DBRef.java b/src/main/java/org/springframework/data/couchbase/repository/support/DBRef.java new file mode 100644 index 000000000..c2ab85942 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/DBRef.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2022 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.support; + +/** + * DB references + * + * @author Michael Reiche + */ +public class DBRef { + Object id; + + public Object getId() { + return id; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/FetchableFluentQuerySupport.java b/src/main/java/org/springframework/data/couchbase/repository/support/FetchableFluentQuerySupport.java new file mode 100644 index 000000000..2ec692bd8 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/FetchableFluentQuerySupport.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2022 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.support; + +import java.util.List; +import java.util.stream.Stream; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.FluentQuery; + +/** + * Querydsl fluent api + * + * @author Michael Reiche + */ +abstract class FetchableFluentQuerySupport implements FluentQuery.FetchableFluentQuery { + + private final P predicate; + private final Sort sort; + private final Class resultType; + private final List fieldsToInclude; + + FetchableFluentQuerySupport(P predicate, Sort sort, Class resultType, List fieldsToInclude) { + this.predicate = predicate; + this.sort = sort; + this.resultType = resultType; + this.fieldsToInclude = fieldsToInclude; + } + + protected abstract FetchableFluentQuerySupport create(P predicate, Sort sort, Class resultType, + List fieldsToInclude); + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#oneValue() + */ + public abstract T oneValue(); + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#firstValue() + */ + public abstract T firstValue(); + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#all() + */ + public abstract List all(); + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#page(org.springframework.data.domain.Pageable) + */ + public abstract Page page(Pageable pageable); + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#stream() + */ + public abstract Stream stream(); + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#count() + */ + public abstract long count(); + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#exists() + */ + public abstract boolean exists(); + + P getPredicate() { + return predicate; + } + + Sort getSort() { + return sort; + } + + Class getResultType() { + return resultType; + } + + List getFieldsToInclude() { + return fieldsToInclude; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/QuerydslCouchbasePredicateExecutor.java b/src/main/java/org/springframework/data/couchbase/repository/support/QuerydslCouchbasePredicateExecutor.java new file mode 100644 index 000000000..2048db9ef --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/QuerydslCouchbasePredicateExecutor.java @@ -0,0 +1,355 @@ +/* + * Copyright 2017-2022 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.support; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.querydsl.EntityPathResolver; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.querydsl.SimpleEntityPathResolver; +import org.springframework.data.repository.query.FluentQuery; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.util.Assert; + +import com.querydsl.core.NonUniqueResultException; +import com.querydsl.core.types.EntityPath; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Predicate; + +/** + * Couchbase-specific {@link QuerydslPredicateExecutor} that allows execution {@link Predicate}s in various forms. + * + * @author Michael Reiche + * @since 5.0 + */ +public class QuerydslCouchbasePredicateExecutor extends QuerydslPredicateExecutorSupport + implements QuerydslPredicateExecutor { + + private final CouchbaseOperations couchbaseOperations; + + /** + * Creates a new {@link QuerydslCouchbasePredicateExecutor} for the given {@link CouchbaseEntityInformation} and + * {@link CouchbaseOperations}. Uses the {@link SimpleEntityPathResolver} to create an {@link EntityPath} for the + * given domain class. + * + * @param entityInformation must not be {@literal null}. + * @param couchbaseOperations must not be {@literal null}. + */ + public QuerydslCouchbasePredicateExecutor(CouchbaseEntityInformation entityInformation, + CouchbaseOperations couchbaseOperations) { + this(entityInformation, couchbaseOperations, SimpleEntityPathResolver.INSTANCE); + } + + /** + * Creates a new {@link QuerydslCouchbasePredicateExecutor} for the given {@link CouchbaseEntityInformation}, + * {@linkCouchbaseOperations} and {@link EntityPathResolver}. + * + * @param entityInformation must not be {@literal null}. + * @param couchbaseOperations must not be {@literal null}. + * @param resolver must not be {@literal null}. + */ + public QuerydslCouchbasePredicateExecutor(CouchbaseEntityInformation entityInformation, + CouchbaseOperations couchbaseOperations, EntityPathResolver resolver) { + super(couchbaseOperations.getConverter(), pathBuilderFor(resolver.createPath(entityInformation.getJavaType())), + entityInformation); + this.couchbaseOperations = couchbaseOperations; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findById(com.querydsl.core.types.Predicate) + */ + @Override + public Optional findOne(Predicate predicate) { + Assert.notNull(predicate, "Predicate must not be null!"); + try { + return Optional.ofNullable(createQueryFor(predicate).fetchOne()); + } catch (NonUniqueResultException ex) { + throw new IncorrectResultSizeDataAccessException(ex.getMessage(), 1, ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findAll(com.querydsl.core.types.Predicate) + */ + @Override + public List findAll(Predicate predicate) { + Assert.notNull(predicate, "Predicate must not be null!"); + return createQueryFor(predicate).fetch(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findAll(com.querydsl.core.types.Predicate, com.querydsl.core.types.OrderSpecifier[]) + */ + @Override + public List findAll(Predicate predicate, OrderSpecifier... orders) { + Assert.notNull(predicate, "Predicate must not be null!"); + Assert.notNull(orders, "Order specifiers must not be null!"); + return createQueryFor(predicate).orderBy(orders).fetch(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findAll(com.querydsl.core.types.Predicate, org.springframework.data.domain.Sort) + */ + @Override + public List findAll(Predicate predicate, Sort sort) { + Assert.notNull(predicate, "Predicate must not be null!"); + Assert.notNull(sort, "Sort must not be null!"); + return applySorting(createQueryFor(predicate), sort).fetch(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findAll(com.querydsl.core.types.OrderSpecifier[]) + */ + @Override + public Iterable findAll(OrderSpecifier... orders) { + Assert.notNull(orders, "Order specifiers must not be null!"); + return createQuery().orderBy(orders).fetch(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findAll(com.querydsl.core.types.Predicate, org.springframework.data.domain.Pageable) + */ + @Override + public Page findAll(Predicate predicate, Pageable pageable) { + Assert.notNull(predicate, "Predicate must not be null!"); + Assert.notNull(pageable, "Pageable must not be null!"); + SpringDataCouchbaseQuery query = createQueryFor(predicate); + return PageableExecutionUtils.getPage(applyPagination(query, pageable).fetch(), pageable, query::fetchCount); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.QuerydslPredicateExecutor#count(com.querydsl.core.types.Predicate) + */ + @Override + public long count(Predicate predicate) { + Assert.notNull(predicate, "Predicate must not be null!"); + return createQueryFor(predicate).fetchCount(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.QuerydslPredicateExecutor#exists(com.querydsl.core.types.Predicate) + */ + @Override + public boolean exists(Predicate predicate) { + Assert.notNull(predicate, "Predicate must not be null!"); + return createQueryFor(predicate).fetchCount() > 0; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.QuerydslPredicateExecutor#findBy(com.querydsl.core.types.Predicate, java.util.function.Function) + */ + @Override + @SuppressWarnings("unchecked") + public R findBy(Predicate predicate, + Function, R> queryFunction) { + Assert.notNull(predicate, "Predicate must not be null!"); + Assert.notNull(queryFunction, "Query function must not be null!"); + return queryFunction.apply(new FluentQuerydsl<>(predicate, (Class) typeInformation().getJavaType())); + } + + /** + * Creates a {@link SpringDataCouchbaseQuery} for the given {@link Predicate}. + * + * @param predicate + * @return + */ + private SpringDataCouchbaseQuery createQueryFor(Predicate predicate) { + return createQuery().where(predicate); + } + + /** + * Creates a {@link SpringDataCouchbaseQuery}. + * + * @return + */ + private SpringDataCouchbaseQuery createQuery() { + return new SpringDataCouchbaseQuery<>(couchbaseOperations, typeInformation().getJavaType()); + } + + /** + * Applies the given {@link Pageable} to the given {@link SpringDataCouchbaseQuery}. + * + * @param query + * @param pageable + * @return + */ + private SpringDataCouchbaseQuery applyPagination(SpringDataCouchbaseQuery query, Pageable pageable) { + if (pageable.isUnpaged()) { + return query; + } + query = query.offset(pageable.getOffset()).limit(pageable.getPageSize()); + return applySorting(query, pageable.getSort()); + } + + /** + * Applies the given {@link Sort} to the given {@link SpringDataCouchbaseQuery}. + * + * @param query + * @param sort + * @return + */ + private SpringDataCouchbaseQuery applySorting(SpringDataCouchbaseQuery query, Sort sort) { + toOrderSpecifiers(sort).forEach(query::orderBy); + return query; + } + + /** + * {@link org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery} using Querydsl + * {@link Predicate}. + * + * @author Mark Paluch + * @since 3.3 + */ + class FluentQuerydsl extends FetchableFluentQuerySupport { + + FluentQuerydsl(Predicate predicate, Class resultType) { + this(predicate, Sort.unsorted(), resultType, Collections.emptyList()); + } + + FluentQuerydsl(Predicate predicate, Sort sort, Class resultType, List fieldsToInclude) { + super(predicate, sort, resultType, fieldsToInclude); + } + + @Override + protected FluentQuerydsl create(Predicate predicate, Sort sort, Class resultType, + List fieldsToInclude) { + return new FluentQuerydsl<>(predicate, sort, resultType, fieldsToInclude); + } + + @Override + public FetchableFluentQuery sortBy(Sort sort) { + return null; + } + + @Override + public FetchableFluentQuery project(Collection properties) { + return null; + } + + @Override + public FetchableFluentQuery as(Class resultType) { + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#oneValue() + */ + @Override + public T oneValue() { + return createQuery().fetchOne(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#firstValue() + */ + @Override + public T firstValue() { + return createQuery().fetchFirst(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#all() + */ + @Override + public List all() { + return createQuery().fetch(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#page(org.springframework.data.domain.Pageable) + */ + @Override + public Page page(Pageable pageable) { + + Assert.notNull(pageable, "Pageable must not be null!"); + + return createQuery().fetchPage(pageable); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#stream() + */ + @Override + public Stream stream() { + return createQuery().stream(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#count() + */ + @Override + public long count() { + return createQuery().fetchCount(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#exists() + */ + @Override + public boolean exists() { + return count() > 0; + } + + private SpringDataCouchbaseQuery createQuery() { + return new SpringDataCouchbaseQuery<>(couchbaseOperations, typeInformation().getJavaType(), getResultType(), + "collection", this::customize).where(getPredicate()); + } + + private void customize(BasicQuery query) { + + List fieldsToInclude = getFieldsToInclude(); + if (!fieldsToInclude.isEmpty()) { + Map fields = new HashMap(); + fieldsToInclude.forEach(field -> fields.put(field, field)); + query.setProjectionFields(fields); + } + + if (getSort().isSorted()) { + query.with(getSort()); + } + } + + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/QuerydslPredicateExecutorSupport.java b/src/main/java/org/springframework/data/couchbase/repository/support/QuerydslPredicateExecutorSupport.java new file mode 100644 index 000000000..88685db55 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/QuerydslPredicateExecutorSupport.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2022 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.support; + +import com.querydsl.core.types.EntityPath; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.PathBuilder; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.domain.Sort; +import org.springframework.data.querydsl.QSort; +import org.springframework.data.repository.core.EntityInformation; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Michael Reiche + */ +public class QuerydslPredicateExecutorSupport { + + private final SpringDataCouchbaseSerializer serializer; + private final PathBuilder builder; + private final EntityInformation entityInformation; + + QuerydslPredicateExecutorSupport(CouchbaseConverter converter, PathBuilder builder, + EntityInformation entityInformation) { + + this.serializer = new SpringDataCouchbaseSerializer(converter); + this.builder = builder; + this.entityInformation = entityInformation; + } + + protected static PathBuilder pathBuilderFor(EntityPath path) { + return new PathBuilder<>(path.getType(), path.getMetadata()); + } + + protected EntityInformation typeInformation() { + return entityInformation; + } + + protected SpringDataCouchbaseSerializer cuchbaseSerializer() { + return serializer; + } + + /** + * Transforms a plain {@link Sort.Order} into a Querydsl specific {@link OrderSpecifier}. + * + * @param order + * @return + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected OrderSpecifier toOrder(Sort.Order order) { + + Expression property = builder.get(order.getProperty()); + + return new OrderSpecifier( + order.isAscending() ? com.querydsl.core.types.Order.ASC : com.querydsl.core.types.Order.DESC, property); + } + + /** + * Converts the given {@link Sort} to {@link OrderSpecifier}. + * + * @param sort + * @return + */ + protected List> toOrderSpecifiers(Sort sort) { + + if (sort instanceof QSort) { + return ((QSort) sort).getOrderSpecifiers(); + } + + return sort.stream().map(this::toOrder).collect(Collectors.toList()); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseQuery.java b/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseQuery.java new file mode 100644 index 000000000..834abe2d2 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseQuery.java @@ -0,0 +1,301 @@ +/* + * Copyright 2012-2022 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.support; + +import static com.couchbase.client.core.io.CollectionIdentifier.DEFAULT_COLLECTION; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.springframework.data.couchbase.core.CouchbaseOperations; +import org.springframework.data.couchbase.core.ExecutableFindByQueryOperation; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.lang.Nullable; + +import com.mysema.commons.lang.CloseableIterator; +import com.mysema.commons.lang.EmptyCloseableIterator; +import com.querydsl.core.Fetchable; +import com.querydsl.core.QueryMetadata; +import com.querydsl.core.QueryModifiers; +import com.querydsl.core.QueryResults; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Predicate; + +/** + * @author Michael Reiche + */ +public class SpringDataCouchbaseQuery extends SpringDataCouchbaseQuerySupport> + implements Fetchable { + + private final CouchbaseOperations couchbaseOperations; + private final Consumer queryCustomizer; + private final ExecutableFindByQueryOperation.ExecutableFindByQuery find;// ExecutableFindOperation.FindWithQuery + // find; + + /** + * Creates a new {@link SpringDataCouchbaseQuery}. + * + * @param operations must not be {@literal null}. + * @param type must not be {@literal null}. + */ + public SpringDataCouchbaseQuery(CouchbaseOperations operations, Class type) { + this(operations, type, DEFAULT_COLLECTION); + } + + /** + * Creates a new {@link SpringDataCouchbaseQuery} to query the given collection. + * + * @param operations must not be {@literal null}. + * @param type must not be {@literal null}. + * @param collectionName must not be {@literal null} or empty. + */ + public SpringDataCouchbaseQuery(CouchbaseOperations operations, Class type, String collectionName) { + this(operations, type, type, collectionName, it -> {}); + } + + /** + * Creates a new {@link SpringDataCouchbaseQuery}. + * + * @param operations must not be {@literal null}. + * @param domainType must not be {@literal null}. + * @param resultType must not be {@literal null}. + * @param collectionName must not be {@literal null} or empty. + * @since 3.3 + */ + SpringDataCouchbaseQuery(CouchbaseOperations operations, Class domainType, Class resultType, + String collectionName, Consumer queryCustomizer) { + super(new SpringDataCouchbaseSerializer(operations.getConverter())); + + Class resultType1 = (Class) resultType; + this.couchbaseOperations = operations; + this.queryCustomizer = queryCustomizer; + this.find = (ExecutableFindByQueryOperation.ExecutableFindByQuery) couchbaseOperations.findByQuery(domainType) + .as(resultType1).inCollection(collectionName); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#iterable() + */ + @Override + public CloseableIterator iterate() { + + try { + Stream stream = stream(); + Iterator iterator = stream.iterator(); + + return new CloseableIterator() { + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public T next() { + return iterator.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Cannot remove from iterator while streaming data."); + } + + @Override + public void close() { + stream.close(); + } + }; + } catch (RuntimeException e) { + return handleException(e, new EmptyCloseableIterator<>()); + } + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#iterable() + */ + @Override + public Stream stream() { + + try { + return find.matching(createQuery()).stream(); + } catch (RuntimeException e) { + return handleException(e, Stream.empty()); + } + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetch() + */ + @Override + public List fetch() { + try { + return find.matching(createQuery()).all(); + } catch (RuntimeException e) { + return handleException(e, Collections.emptyList()); + } + } + + /** + * Fetch a {@link Page}. + * + * @param pageable + * @return + */ + public Page fetchPage(Pageable pageable) { + + try { + + List content = find.matching(createQuery().with(pageable)).all(); + + return PageableExecutionUtils.getPage(content, pageable, this::fetchCount); + } catch (RuntimeException e) { + return handleException(e, new PageImpl<>(Collections.emptyList(), pageable, 0)); + } + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetchFirst() + */ + @Override + public T fetchFirst() { + try { + return find.matching(createQuery()).firstValue(); + } catch (RuntimeException e) { + return handleException(e, null); + } + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetchOne() + */ + @Override + public T fetchOne() { + try { + return find.matching(createQuery()).oneValue(); + } catch (RuntimeException e) { + return handleException(e, null); + } + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetchResults() + */ + @Override + public QueryResults fetchResults() { + + long total = fetchCount(); + return total > 0L ? new QueryResults<>(fetch(), getQueryMixin().getMetadata().getModifiers(), total) + : QueryResults.emptyResults(); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetchCount() + */ + @Override + public long fetchCount() { + try { + return find.matching(createQuery().skip(-1).limit(-1)).count(); + } catch (RuntimeException e) { + return handleException(e, 0L); + } + } + + protected org.springframework.data.couchbase.core.query.Query createQuery() { + + QueryMetadata metadata = getQueryMixin().getMetadata(); + + return createQuery(createFilter(metadata), metadata.getProjection(), metadata.getModifiers(), + metadata.getOrderBy()); + } + + @Override + protected Predicate createFilter(QueryMetadata metadata) { + return metadata.getWhere(); + } + + @Override + protected List getIds(Class var1, Predicate var2) { + return null; + } + + protected org.springframework.data.couchbase.core.query.Query createQuery(@Nullable Predicate filter, + @Nullable Expression projection, QueryModifiers modifiers, List> orderBy) { + + Map fields = createProjection(projection); + BasicQuery basicQuery = new BasicQuery(createCriteria(filter), fields); + + Integer limit = modifiers.getLimitAsInteger(); + Integer offset = modifiers.getOffsetAsInteger(); + + if (limit != null) { + basicQuery.limit(limit); + } + if (offset != null) { + basicQuery.skip(offset); + } + if (orderBy.size() > 0) { + basicQuery.setSort(createSort(orderBy)); + } + queryCustomizer.accept(basicQuery); + return basicQuery; + } + + // @Override + // protected CouchbaseDocument createQuery(Predicate filter); + + // @Override + // protected Map createProjection(Expression projection); + + // @Override + // protected CouchbaseDocument createSort(List> orderBy); + + /* + * Fetch the list of ids matching a given condition. + * + * @param targetType must not be {@literal null}. + * @param condition must not be {@literal null}. + * @return empty {@link List} if none found. + + protected List getIds(Class targetType, Predicate condition) { + Query query = createQuery(condition, null, QueryModifiers.EMPTY, Collections.emptyList()); + return couchbaseOperations.findByQuery(targetType).matching(query).all(); // findDistinct(query, "_id", targetType, Object.class); + } + */ + private static T handleException(RuntimeException e, T defaultValue) { + + if (e.getClass().getName().endsWith("$NoResults")) { + return defaultValue; + } + + throw e; + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseQuerySupport.java b/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseQuerySupport.java new file mode 100644 index 000000000..465fe52ad --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseQuerySupport.java @@ -0,0 +1,133 @@ +/* + * Copyright 2012-2022 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.support; + +import java.util.List; +import java.util.Map; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.domain.Sort; + +import com.querydsl.core.support.QueryMixin; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.couchbase.document.AbstractCouchbaseQueryDSL; +import com.querydsl.couchbase.document.CouchbaseDocumentSerializer; + +/** + * @author Michael Reiche + */ +abstract class SpringDataCouchbaseQuerySupport> + extends AbstractCouchbaseQueryDSL { + + private final QueryMixin superQueryMixin; + + // TODO private static final JsonWriterSettings JSON_WRITER_SETTINGS = + // JsonWriterSettings.builder().outputMode(JsonMode.SHELL) + // .build(); + + private final CouchbaseDocumentSerializer serializer; + + @SuppressWarnings("unchecked") + SpringDataCouchbaseQuerySupport(CouchbaseDocumentSerializer serializer) { + super(serializer); + this.serializer = serializer; + DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(this); + this.superQueryMixin = (QueryMixin) fieldAccessor.getPropertyValue("queryMixin"); + } + + /** + * Returns the representation of the query.
+ * The following query + * + *
+	 *
+	 * where(p.lastname.eq("Matthews")).orderBy(p.firstname.asc()).offset(1).limit(5);
+	 * 
+ * + * results in + * + *
+	 *
+	 * find({"lastname" : "Matthews"}).sort({"firstname" : 1}).skip(1).limit(5)
+	 * 
+ * + * Note that encoding to {@link String} may fail when using data types that cannot be encoded or DBRef's without an + * identifier. + * + * @return never {@literal null}. + */ + @Override + public String toString() { + + Map projection = createProjection(getQueryMixin().getMetadata().getProjection()); + Sort sort = createSort(getQueryMixin().getMetadata().getOrderBy()); + // TODO DocumentCodec codec = new DocumentCodec(ClientSettings.getDefaultCodecRegistry()); + + // TODO + // StringBuilder sb = new StringBuilder("find(" + asDocument().toJson(JSON_WRITER_SETTINGS, codec)); + StringBuilder sb = new StringBuilder("find(" + asDocument().toString()); + // if (projection != null && projection.isEmpty()) { + // sb.append(", ").append(projection.toJson(JSON_WRITER_SETTINGS, codec)); + // } + sb.append(")"); + // TODO + // if (!sort.isEmpty()) { + // sb.append(".sort(").append(sort.toJson(JSON_WRITER_SETTINGS, codec)).append(")"); + // } + if (getQueryMixin().getMetadata().getModifiers().getOffset() != null) { + sb.append(".skip(").append(getQueryMixin().getMetadata().getModifiers().getOffset()).append(")"); + } + if (getQueryMixin().getMetadata().getModifiers().getLimit() != null) { + sb.append(".limit(").append(getQueryMixin().getMetadata().getModifiers().getLimit()).append(")"); + } + return sb.toString(); + } + + /** + * Get the where definition as a Document instance + * + * @return + */ + public CouchbaseDocument asDocument() { + return createQuery(getQueryMixin().getMetadata().getWhere()); + } + + /** + * Obtain the json query representation. + * + * @return never {@literal null}. public String toJson() { return toJson(JSON_WRITER_SETTINGS); } + */ + + /** + * Obtain the json query representation applying given {@link JsonWriterSettings settings}. + * + * @param settings must not be {@literal null}. + * @return never {@literal null}. public String toJson(JsonWriterSettings settings) { return + * asDocument().toJson(settings); } + */ + + /** + * Compute the sort {@link CouchbaseDocument} from the given list of {@link OrderSpecifier order specifiers}. + * + * @param orderSpecifiers can be {@literal null}. + * @return an empty {@link CouchbaseDocument} if predicate is {@literal null}. see + * CouchbaseDocumentSerializer#toSort(List) + */ + protected Sort createSort(List> orderSpecifiers) { + return null; // TODO serializer.toSort(orderSpecifiers); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseSerializer.java b/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseSerializer.java new file mode 100644 index 000000000..2189ea89d --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/repository/support/SpringDataCouchbaseSerializer.java @@ -0,0 +1,226 @@ +/* + * Copyright 2011-2022 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.support; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; + +import com.querydsl.couchbase.document.CouchbaseDocumentSerializer; +import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition; +import org.springframework.data.mapping.context.MappingContext; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +import com.querydsl.core.types.Constant; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.Operation; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.PathMetadata; +import com.querydsl.core.types.PathType; + +/** + * Custom {@link CouchbaseDocumentSerializer} to take mapping information into account when building keys for + * constraints. + * + * @author Michael Reiche + */ +public class SpringDataCouchbaseSerializer extends CouchbaseDocumentSerializer { + + private static final String ID_KEY = "_id"; + private static final Set PATH_TYPES; + + static { + + Set pathTypes = new HashSet<>(); + pathTypes.add(PathType.VARIABLE); + pathTypes.add(PathType.PROPERTY); + + PATH_TYPES = Collections.unmodifiableSet(pathTypes); + } + + private final CouchbaseConverter converter; + private final MappingContext, CouchbasePersistentProperty> mappingContext; + // private final QueryMapper mapper; + + /** + * Creates a new {@link SpringDataCouchbaseSerializer} for the given {@link CouchbaseConverter}. + * + * @param converter must not be {@literal null}. + */ + public SpringDataCouchbaseSerializer(CouchbaseConverter converter) { + + Assert.notNull(converter, "CouchbaseConverter must not be null!"); + + this.mappingContext = converter.getMappingContext(); + this.converter = converter; + // this.mapper = new QueryMapper(converter); + } + + /* + * (non-Javadoc) + * @see com.querydsl.couchbase.CouchbaseSerializer#visit(com.querydsl.core.types.Constant, java.lang.Void) + */ + @Override + public Object visit(Constant expr, Void context) { + + if (!ClassUtils.isAssignable(Enum.class, expr.getType())) { + return super.visit(expr, context); + } + + return converter.convertForWriteIfNeeded(expr.getConstant()); + } + + /* + * (non-Javadoc) + * @see com.querydsl.couchbase.CouchbaseSerializer#getKeyForPath(com.querydsl.core.types.Path, com.querydsl.core.types.PathMetadata) + */ + @Override + protected String getKeyForPath(Path expr, PathMetadata metadata) { + // TODO - substitutions for meta().id, meta().expiry, meta().cas + if (!metadata.getPathType().equals(PathType.PROPERTY)) { + return super.getKeyForPath(expr, metadata); + } + + Path parent = metadata.getParent(); + CouchbasePersistentEntity entity = mappingContext.getRequiredPersistentEntity(parent.getType()); + CouchbasePersistentProperty property = entity.getPersistentProperty(metadata.getName()); + + return property == null ? super.getKeyForPath(expr, metadata) : property.getFieldName(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.couchbase.repository.support.CouchbaseSerializer#asDocument(java.lang.String, java.lang.Object) + */ + @Override + protected QueryCriteriaDefinition asDocument(@Nullable String key, @Nullable Object value) { + + value = value instanceof Optional ? ((Optional) value).orElse(null) : value; + + return super.asDocument(key, value instanceof Pattern ? value : converter.convertForWriteIfNeeded(value)); + } + + /* + * (non-Javadoc) + * @see com.querydsl.couchbase.CouchbaseSerializer#isReference(com.querydsl.core.types.Path) + */ + @Override + protected boolean isReference(@Nullable Path path) { + + CouchbasePersistentProperty property = getPropertyForPotentialDbRef(path); + return property == null ? false : property.isAssociation(); + } + + /* + * (non-Javadoc) + * @see com.querydsl.couchbase.CouchbaseSerializer#asReference(java.lang.Object) + */ + @Override + protected DBRef asReference(@Nullable Object constant) { + return asReference(constant, null); + } + + protected DBRef asReference(Object constant, Path path) { + return null; // converter.toDBRef(constant, getPropertyForPotentialDbRef(path)); + } + + /* + * (non-Javadoc) + * @see com.querydsl.couchbase.CouchbaseSerializer#asDBKey(com.querydsl.core.types.Operation, int) + */ + @Override + protected String asDBKey(@Nullable Operation expr, int index) { + + Expression arg = expr.getArg(index); + String key = super.asDBKey(expr, index); + + if (!(arg instanceof Path)) { + return key; + } + + Path path = (Path) arg; + + if (!isReference(path)) { + return key; + } + + CouchbasePersistentProperty property = getPropertyFor(path); + + return property.isIdProperty() ? key.replaceAll("." + ID_KEY + "$", "") : key; + } + + /* + * (non-Javadoc) + * @see com.querydsl.couchbase.CouchbaseSerializer#convert(com.querydsl.core.types.Path, com.querydsl.core.types.Constant) + */ + protected Object convert(@Nullable Path path, @Nullable Constant constant) { + + if (!isReference(path)) { + return null; + // return super.convert(path, constant); + } + + CouchbasePersistentProperty property = getPropertyFor(path); + + return property.isIdProperty() ? asReference(constant.getConstant(), path.getMetadata().getParent()) + : asReference(constant.getConstant(), path); + } + + @Nullable + private CouchbasePersistentProperty getPropertyFor(Path path) { + + Path parent = path.getMetadata().getParent(); + + if (parent == null || !PATH_TYPES.contains(path.getMetadata().getPathType())) { + return null; + } + + CouchbasePersistentEntity entity = mappingContext.getPersistentEntity(parent.getType()); + return entity != null ? entity.getPersistentProperty(path.getMetadata().getName()) : null; + } + + /** + * Checks the given {@literal path} for referencing the {@literal id} property of a {@link DBRef} referenced object. + * If so it returns the referenced {@link CouchbasePersistentProperty} of the {@link DBRef} instead of the + * {@literal id} property. + * + * @param path + * @return + */ + private CouchbasePersistentProperty getPropertyForPotentialDbRef(Path path) { + + if (path == null) { + return null; + } + + CouchbasePersistentProperty property = getPropertyFor(path); + PathMetadata metadata = path.getMetadata(); + + if (property != null && property.isIdProperty() && metadata != null && metadata.getParent() != null) { + return getPropertyFor(metadata.getParent()); + } + + return property; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntityTests.java b/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntityTests.java index d81dfba89..e6cebd1b4 100644 --- a/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntityTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntityTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2020 the original author or authors. + * Copyright 2013-2022 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. @@ -190,78 +190,78 @@ private BasicCouchbasePersistentEntity getBasicCouchbasePersistentEntity(Class $2 or `country` = $3)", c.export(new int[1], parameters, null)); + assertEquals(" (`name` = $1) and (`age` > $2 or `country` = $3)", c.export(new int[1], parameters, null)); assertEquals("[\"Bubba\",12,\"Austria\"]", parameters.toString()); } @@ -77,19 +78,33 @@ void testNestedAndCriteria() { void testNestedOrCriteria() { QueryCriteria c = where(i("name")).is("Bubba").or(where(i("age")).gt(12).or(i("country")).is("Austria")); JsonArray parameters = JsonArray.create(); - assertEquals("`name` = $1 or (`age` > $2 or `country` = $3)", c.export(new int[1], parameters, null)); + assertEquals(" (`name` = $1) or (`age` > $2 or `country` = $3)", c.export(new int[1], parameters, null)); assertEquals("[\"Bubba\",12,\"Austria\"]", parameters.toString()); } @Test void testNestedNotIn() { - QueryCriteria c = where(i("name")).is("Bubba").or(where(i("age")).gt(12).or(i("country")).is("Austria")) - .and(where(i("state")).notIn((Object) new String[] { "Alabama", "Florida" })); + QueryCriteria c = where(i("name")).is("Bubba").or(where(i("age")).gt(12).and(i("country")).is("Austria")) + .and(where(i("state")).notIn(new String[] { "Alabama", "Florida" })); JsonArray parameters = JsonArray.create(); - assertEquals("`name` = $1 or (`age` > $2 or `country` = $3) and (not( (`state` in $4) ))", + assertEquals(" ( (`name` = $1) or (`age` > $2 and `country` = $3)) and (not( (`state` in $4) ))", c.export(new int[1], parameters, null)); } + @Test + void testNestedNotIn2() { + QueryCriteria c = where(i("name")).is("Bubba").or(where(i("age")).gt(12)).and(where(i("state")).eq("1")); + JsonArray parameters = JsonArray.create(); + assertEquals(" ( (`name` = $1) or (`age` > $2)) and (`state` = $3)", c.export(new int[1], parameters, null)); + } + + @Test + void testNestedNotIn3() { + QueryCriteria c = where(i("name")).is("Bubba").or(where(i("age")).gt(12)).and(i("state")).eq("1"); + JsonArray parameters = JsonArray.create(); + assertEquals(" (`name` = $1) or (`age` > $2) and `state` = $3", c.export(new int[1], parameters, null)); + } + @Test void testLt() { QueryCriteria c = where(i("name")).lt("Couch"); @@ -252,7 +267,7 @@ void testTrue() { @Test void testFalse() { QueryCriteria c = where(i("name")).FALSE(); - assertEquals("not( (`name`) )", c.export()); + assertEquals("not(`name`)", c.export()); } @Test diff --git a/src/test/java/org/springframework/data/couchbase/domain/Airline.java b/src/test/java/org/springframework/data/couchbase/domain/Airline.java index 157669558..3e2b0e4d5 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Airline.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Airline.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors + * Copyright 2012-2022 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. @@ -24,15 +24,21 @@ @Document @CompositeQueryIndex(fields = { "id", "name desc" }) @CompositeQueryIndex(fields = { "id.something", "name desc" }) +/** + * @author Michael Reiche + */ public class Airline extends ComparableEntity { @Id String id; @QueryIndexed String name; + String hqCountry; + @PersistenceConstructor - public Airline(String id, String name) { + public Airline(String id, String name, String hqCountry) { this.id = id; this.name = name; + this.hqCountry = hqCountry; } public String getId() { @@ -43,4 +49,8 @@ public String getName() { return name; } + public String getHqCountry() { + return hqCountry; + } + } diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirlineRepository.java b/src/test/java/org/springframework/data/couchbase/domain/AirlineRepository.java index ce7b16662..598a6682f 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/AirlineRepository.java +++ b/src/test/java/org/springframework/data/couchbase/domain/AirlineRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,23 @@ package org.springframework.data.couchbase.domain; +import java.util.List; + +import org.springframework.data.couchbase.repository.DynamicProxyable; import org.springframework.data.couchbase.repository.Query; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.util.List; - +/** + * @author Michael Reiche + */ @Repository -public interface AirlineRepository extends PagingAndSortingRepository { +public interface AirlineRepository extends PagingAndSortingRepository, + QuerydslPredicateExecutor, DynamicProxyable { @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (name = $1)") - List getByName(@Param("airline_name")String airlineName); + List getByName(@Param("airline_name") String airlineName); } diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java index 7143bb269..7b310fb50 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java @@ -115,7 +115,7 @@ void saveReplaceUpsertInsert() { userRepository.delete(user); // Airline does not have a version - Airline airline = new Airline(UUID.randomUUID().toString(), "MyAirline"); + Airline airline = new Airline(UUID.randomUUID().toString(), "MyAirline", null); // save the document - we don't care how on this call airlineRepository.save(airline); airlineRepository.save(airline); // If it was an insert it would fail. Can't tell if it is an upsert or replace. diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java index b24ff3aae..e5d5b1734 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -67,6 +67,7 @@ import org.springframework.data.couchbase.core.query.N1QLExpression; import org.springframework.data.couchbase.core.query.QueryCriteria; import org.springframework.data.couchbase.domain.Address; +import org.springframework.data.couchbase.domain.AirlineRepository; import org.springframework.data.couchbase.domain.Airport; import org.springframework.data.couchbase.domain.AirportMini; import org.springframework.data.couchbase.domain.AirportRepository; @@ -124,6 +125,8 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr @Autowired AirportRepository airportRepository; + @Autowired AirlineRepository airlineRepository; + @Autowired UserRepository userRepository; @Autowired UserSubmissionRepository userSubmissionRepository; diff --git a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java index 46169ba59..aafb53558 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java @@ -76,7 +76,7 @@ void saveReplaceUpsertInsert() { userRepository.delete(user); // Airline does not have a version - Airline airline = new Airline(UUID.randomUUID().toString(), "MyAirline"); + Airline airline = new Airline(UUID.randomUUID().toString(), "MyAirline", null); // save the document - we don't care how on this call airlineRepository.save(airline).block(); airlineRepository.save(airline).block(); // If it was an insert it would fail. Can't tell if an upsert or replace. diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuerydslIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuerydslIntegrationTests.java new file mode 100644 index 000000000..9164887ec --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQuerydslIntegrationTests.java @@ -0,0 +1,647 @@ +/* + * Copyright 2017-2022 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 com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.springframework.data.couchbase.util.Util.comprises; +import static org.springframework.data.couchbase.util.Util.exactly; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Locale; +import java.util.Optional; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.mapping.event.ValidatingCouchbaseEventListener; +import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition; +import org.springframework.data.couchbase.domain.Airline; +import org.springframework.data.couchbase.domain.AirlineRepository; +import org.springframework.data.couchbase.domain.NaiveAuditorAware; +import org.springframework.data.couchbase.domain.QAirline; +import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider; +import org.springframework.data.couchbase.repository.auditing.EnableCouchbaseAuditing; +import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories; +import org.springframework.data.couchbase.repository.support.BasicQuery; +import org.springframework.data.couchbase.repository.support.SpringDataCouchbaseSerializer; +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.couchbase.util.JavaIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +import com.couchbase.client.java.env.ClusterEnvironment; +import com.couchbase.client.java.query.QueryScanConsistency; +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.BooleanExpression; + +/** + * Repository tests + * + * @author Michael Reiche + */ +@SpringJUnitConfig(CouchbaseRepositoryQuerydslIntegrationTests.Config.class) +@IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) +public class CouchbaseRepositoryQuerydslIntegrationTests extends JavaIntegrationTests { + + @Autowired AirlineRepository airlineRepository; + + static QAirline airline = QAirline.airline; + // saved + static Airline united = new Airline("1", "United Airlines", "US"); + static Airline lufthansa = new Airline("2", "Lufthansa", "DE"); + static Airline emptyStringAirline = new Airline("3", "Empty String", ""); + static Airline nullStringAirline = new Airline("4", "Null String", null); + static Airline unitedLowercase = new Airline("5", "united airlines", "US"); + static Airline[] saved = new Airline[] { united, lufthansa, emptyStringAirline, nullStringAirline, unitedLowercase }; + // not saved + static Airline flyByNight = new Airline("1001", "Fly By Night", "UK"); + static Airline sleepByDay = new Airline("1002", "Sleep By Day", "CA"); + static Airline[] notSaved = new Airline[] { flyByNight, sleepByDay }; + + SpringDataCouchbaseSerializer serializer = new SpringDataCouchbaseSerializer(couchbaseTemplate.getConverter()); + + @BeforeAll + static public void beforeAll() { + callSuperBeforeAll(new Object() {}); + ApplicationContext ac = new AnnotationConfigApplicationContext( + CouchbaseRepositoryQuerydslIntegrationTests.Config.class); + CouchbaseTemplate template = (CouchbaseTemplate) ac.getBean("couchbaseTemplate"); + for (Airline airline : saved) { + template.insertById(Airline.class).one(airline); + } + template.findByQuery(Airline.class).withConsistency(REQUEST_PLUS).all(); + } + + @AfterAll + static public void afterAll() { + ApplicationContext ac = new AnnotationConfigApplicationContext( + CouchbaseRepositoryQuerydslIntegrationTests.Config.class); + CouchbaseTemplate template = (CouchbaseTemplate) ac.getBean("couchbaseTemplate"); + for (Airline airline : saved) { + template.removeById(Airline.class).one(airline.getId()); + } + template.findByQuery(Airline.class).withConsistency(REQUEST_PLUS).all(); + callSuperAfterAll(new Object() {}); + } + + @Test + void testEq() { + { + BooleanExpression predicate = airline.name.eq(flyByNight.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> a.getName().equals(flyByNight.getName())).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name = $1", bq(predicate)); + + } + { + BooleanExpression predicate = airline.name.eq(united.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> a.getName().equals(united.getName())).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name = $1", bq(predicate)); + } + } + + // this gives hqCountry == "" and hqCountry is missing + // @Test + void testStringIsEmpty() { + { + BooleanExpression predicate = airline.hqCountry.isEmpty(); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, emptyStringAirline, nullStringAirline), "[unexpected] -> [missing]"); + assertEquals(" WHERE UPPER(name) like $1", bq(predicate)); + } + } + + @Test + void testNot() { + { + BooleanExpression predicate = airline.name.eq(united.getName()).and(airline.hqCountry.eq(united.getHqCountry())) + .not(); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> !(a.getName().equals(united.getName()) && a.getHqCountry().equals(united.getHqCountry()))) + .toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE not( ( (hqCountry = $1) and (name = $2)) )", bq(predicate)); + } + { + BooleanExpression predicate = airline.name.in(Arrays.asList(united.getName())).not(); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> !(a.getName().equals(united.getName()))).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE not( (name = $1) )", bq(predicate)); + } + { + BooleanExpression predicate = airline.name.eq(united.getName()).not(); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> !(a.getName().equals(united.getName()))).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE not( (name = $1) )", bq(predicate)); + } + } + + @Test + void testAnd() { + { + BooleanExpression predicate = airline.name.eq(united.getName()).and(airline.hqCountry.eq(united.getHqCountry())); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().equals(united.getName()) && a.getHqCountry().equals(united.getHqCountry())) + .toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE (name = $1) and (hqCountry = $2)", bq(predicate)); + } + { + BooleanExpression predicate = airline.name.eq(united.getName()) + .and(airline.hqCountry.eq(lufthansa.getHqCountry())); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().equals(united.getName()) && a.getHqCountry().equals(lufthansa.getHqCountry())) + .toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE (name = $1) and (hqCountry = $2)", bq(predicate)); + } + } + + @Test + void testOr() { + { + BooleanExpression predicate = airline.name.eq(united.getName()) + .or(airline.hqCountry.eq(lufthansa.getHqCountry())); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().equals(united.getName()) || a.getName().equals(lufthansa.getName())) + .toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE (name = $1) or (hqCountry = $2)", bq(predicate)); + } + } + + @Test + void testNe() { + { + BooleanExpression predicate = airline.name.ne(united.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> !a.getName().equals(united.getName())).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name != $1", bq(predicate)); + } + } + + @Test + void testStartsWith() { + { + BooleanExpression predicate = airline.name.startsWith(united.getName().substring(0, 5)); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, Arrays.stream(saved) + .filter(a -> a.getName().startsWith(united.getName().substring(0, 5))).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name like ($1||\"%\")", bq(predicate)); + } + } + + @Test + void testStartsWithIgnoreCase() { + { + BooleanExpression predicate = airline.name.startsWithIgnoreCase(united.getName().substring(0, 5)); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().toUpperCase().startsWith(united.getName().toUpperCase().substring(0, 5))) + .toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE UPPER(name) like ($1||\"%\")", bq(predicate)); + } + } + + @Test + void testEndsWith() { + { + BooleanExpression predicate = airline.name.endsWith(united.getName().substring(1)); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, Arrays.stream(saved).filter(a -> a.getName().endsWith(united.getName().substring(1))) + .toArray(Airline[]::new)), "[unexpected] -> [missing]"); + assertEquals(" WHERE name like (\"%\"||$1)", bq(predicate)); + + } + } + + @Test + void testEndsWithIgnoreCase() { + { + BooleanExpression predicate = airline.name.endsWithIgnoreCase(united.getName().substring(1)); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().toUpperCase().endsWith(united.getName().toUpperCase().substring(1))) + .toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE UPPER(name) like (\"%\"||$1)", bq(predicate)); + } + } + + @Test + void testEqIgnoreCase() { + { + BooleanExpression predicate = airline.name.equalsIgnoreCase(flyByNight.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved).filter(a -> a.getName().equalsIgnoreCase(flyByNight.getName())).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE UPPER(name) = $1", bq(predicate)); + } + { + BooleanExpression predicate = airline.name.equalsIgnoreCase(united.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> a.getName().equalsIgnoreCase(united.getName())).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE UPPER(name) = $1", bq(predicate)); + } + + } + + @Test + void testContains() { + { + BooleanExpression predicate = airline.name.contains("United"); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, Arrays.stream(saved).filter(a -> a.getName().contains("United")).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE contains(name, $1)", bq(predicate)); + } + } + + @Test + void testContainsIgnoreCase() { + { + BooleanExpression predicate = airline.name.containsIgnoreCase("united"); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().toUpperCase(Locale.ROOT).contains("united".toUpperCase(Locale.ROOT))) + .toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE contains(UPPER(name), $1)", bq(predicate)); + + } + } + + @Test + void testLike() { + { + BooleanExpression predicate = airline.name.like("%nited%"); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, Arrays.stream(saved).filter(a -> a.getName().contains("nited")).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name like $1", bq(predicate)); + } + } + + @Test + void testLikeIgnoreCase() { + { + BooleanExpression predicate = airline.name.likeIgnoreCase("%Airlines"); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().toUpperCase(Locale.ROOT).endsWith("Airlines".toUpperCase(Locale.ROOT))) + .toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE UPPER(name) like $1", bq(predicate)); + } + } + + // This is 'between' is inclusive + @Test + void testBetween() { + { + BooleanExpression predicate = airline.name.between(flyByNight.getName(), united.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter( + a -> a.getName().compareTo(flyByNight.getName()) >= 0 && a.getName().compareTo(united.getName()) <= 0) + .toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name between $1 and $2", bq(predicate)); + } + } + + @Test + void testIn() { + { + BooleanExpression predicate = airline.name.in(Arrays.asList(united.getName())); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> a.getName().equals(united.getName())).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name = $1", bq(predicate)); + } + + { + BooleanExpression predicate = airline.name.in(Arrays.asList(united.getName(), lufthansa.getName())); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().equals(united.getName()) || a.getName().equals(lufthansa.getName())) + .toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name in $1", bq(predicate)); + } + + { + BooleanExpression predicate = airline.name.in("Fly By Night", "Sleep By Day"); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> a.getName().equals(flyByNight.getName()) || a.getName().equals(sleepByDay.getName())) + .toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name in $1", bq(predicate)); + } + } + + @Test + void testNotIn() { + { + BooleanExpression predicate = airline.name.notIn("Fly By Night", "Sleep By Day"); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved) + .filter(a -> !(a.getName().equals(flyByNight.getName()) || a.getName().equals(sleepByDay.getName()))) + .toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE not( (name in $1) )", bq(predicate)); + } + { + BooleanExpression predicate = airline.name.notIn(united.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> !a.getName().equals(united.getName())).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name != $1", bq(predicate)); + } + } + + @Test + @Disabled + void testColIsEmpty() {} + + @Test + void testLt() { + { + BooleanExpression predicate = airline.name.lt(lufthansa.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> a.getName().compareTo(lufthansa.getName()) < 0).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name < $1", bq(predicate)); + } + } + + @Test + void testGt() { + { + BooleanExpression predicate = airline.name.gt(lufthansa.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull( + comprises(result, + Arrays.stream(saved).filter(a -> a.getName().compareTo(lufthansa.getName()) > 0).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name > $1", bq(predicate)); + } + } + + @Test + void testLoe() { + { + BooleanExpression predicate = airline.name.loe(lufthansa.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved).filter(a -> a.getName().compareTo(lufthansa.getName()) <= 0).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name <= $1", bq(predicate)); + } + } + + @Test + void testGoe() { + { + BooleanExpression predicate = airline.name.goe(lufthansa.getName()); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved).filter(a -> a.getName().compareTo(lufthansa.getName()) >= 0).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE name >= $1", bq(predicate)); + } + } + + // when hqCountry == null, no value is stored therefore isNull is false. Only hqCountry:null gives isNull + // and we don't have that. Conversely, only hqCountry has a value (which is not 'null') gives isNotNull + // so isNull and isNotNull are *not* compliments + @Test + @Disabled + void testIsNull() { + { + BooleanExpression predicate = airline.hqCountry.isNull(); + Optional result = airlineRepository.findOne(predicate); + assertNull(exactly(result, nullStringAirline), "[unexpected] -> [missing]"); + assertEquals(" WHERE name = $1", bq(predicate)); + } + } + + // when hqCountry == null, no value is stored therefore isNull is false. Only hqCountry:null gives isNull + // and we don't have that. Conversely, only hqCountry has a value (which is not 'null') gives isNotNull + // so isNull and isNotNull are *not* compliments + @Test + void testIsNotNull() { + { + BooleanExpression predicate = airline.hqCountry.isNotNull(); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, Arrays.stream(saved).filter(a -> a.getHqCountry() != null).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE hqCountry is not null", bq(predicate)); + } + } + + @Test + @Disabled + void testContainsKey() {} + + @Test + void testStringLength() { + { + BooleanExpression predicate = airline.name.length().eq(united.getName().length()); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, + Arrays.stream(saved).filter(a -> a.getName().length() == united.getName().length()).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE LENGTH(name) = $1", bq(predicate)); + } + { + BooleanExpression predicate = airline.name.length().eq(flyByNight.getName().length()); + Iterable result = airlineRepository.findAll(predicate); + assertNull(comprises(result, Arrays.stream(saved) + .filter(a -> a.getName().length() == flyByNight.getName().length()).toArray(Airline[]::new)), + "[unexpected] -> [missing]"); + assertEquals(" WHERE LENGTH(name) = $1", bq(predicate)); + } + } + + private void sleep(int millis) { + try { + Thread.sleep(millis); // so they are executed out-of-order + } catch (InterruptedException ie) { + ; + } + } + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + @EnableCouchbaseAuditing(dateTimeProviderRef = "dateTimeProviderRef") + static class Config extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return connectionString(); + } + + @Override + public String getUserName() { + return config().adminUsername(); + } + + @Override + public String getPassword() { + return config().adminPassword(); + } + + @Override + public String getBucketName() { + return bucketName(); + } + + @Bean(name = "auditorAwareRef") + public NaiveAuditorAware testAuditorAware() { + return new NaiveAuditorAware(); + } + + @Override + public void configureEnvironment(final ClusterEnvironment.Builder builder) { + builder.ioConfig().maxHttpConnections(11).idleHttpConnectionTimeout(Duration.ofSeconds(4)); + return; + } + + @Bean(name = "dateTimeProviderRef") + public DateTimeProvider testDateTimeProvider() { + return new AuditingDateTimeProvider(); + } + + @Bean + public LocalValidatorFactoryBean validator() { + return new LocalValidatorFactoryBean(); + } + + @Bean + public ValidatingCouchbaseEventListener validationEventListener() { + return new ValidatingCouchbaseEventListener(validator()); + } + } + + String bq(Predicate predicate) { + BasicQuery basicQuery = new BasicQuery((QueryCriteriaDefinition) serializer.handle(predicate), null); + return basicQuery.export(new int[1]); + } + + @Configuration + @EnableCouchbaseRepositories("org.springframework.data.couchbase") + @EnableCouchbaseAuditing(auditorAwareRef = "auditorAwareRef", dateTimeProviderRef = "dateTimeProviderRef") + static class ConfigRequestPlus extends AbstractCouchbaseConfiguration { + + @Override + public String getConnectionString() { + return connectionString(); + } + + @Override + public String getUserName() { + return config().adminUsername(); + } + + @Override + public String getPassword() { + return config().adminPassword(); + } + + @Override + public String getBucketName() { + return bucketName(); + } + + @Bean(name = "auditorAwareRef") + public NaiveAuditorAware testAuditorAware() { + return new NaiveAuditorAware(); + } + + @Bean(name = "dateTimeProviderRef") + public DateTimeProvider testDateTimeProvider() { + return new AuditingDateTimeProvider(); + } + + @Override + public QueryScanConsistency getDefaultConsistency() { + return REQUEST_PLUS; + } + } +} 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 index 72f657d2c..382d86a53 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/StringN1qlQueryCreatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2022 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. @@ -23,7 +23,6 @@ import java.util.Properties; import java.util.UUID; -import com.couchbase.client.java.query.QueryScanConsistency; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; @@ -58,6 +57,8 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import com.couchbase.client.java.query.QueryScanConsistency; + /** * @author Michael Nitschinger * @author Michael Reiche @@ -82,7 +83,7 @@ public void beforeEach() { @Test @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) void findUsingStringNq1l() throws Exception { - Airline airline = new Airline(UUID.randomUUID().toString(), "Continental"); + Airline airline = new Airline(UUID.randomUUID().toString(), "Continental", "USA"); try { Airline modified = couchbaseTemplate.upsertById(Airline.class).one(airline); @@ -99,8 +100,8 @@ queryMethod, converter, config().bucketname(), new SpelExpressionParser(), Query query = creator.createQuery(); - ExecutableFindByQuery q = (ExecutableFindByQuery) couchbaseTemplate - .findByQuery(Airline.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(query); + ExecutableFindByQuery q = (ExecutableFindByQuery) couchbaseTemplate.findByQuery(Airline.class) + .withConsistency(QueryScanConsistency.REQUEST_PLUS).matching(query); Optional al = q.one(); assertEquals(airline.toString(), al.get().toString()); diff --git a/src/test/java/org/springframework/data/couchbase/util/Util.java b/src/test/java/org/springframework/data/couchbase/util/Util.java index 06710bb90..31c07db84 100644 --- a/src/test/java/org/springframework/data/couchbase/util/Util.java +++ b/src/test/java/org/springframework/data/couchbase/util/Util.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors + * Copyright 2020-2022 the original author or authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,84 +16,134 @@ package org.springframework.data.couchbase.util; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.awaitility.Awaitility.with; + import java.io.InputStream; import java.time.Duration; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; import java.util.function.BooleanSupplier; import java.util.function.Supplier; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.awaitility.Awaitility.with; +import org.springframework.data.util.Pair; /** * Provides a bunch of utility APIs that help with testing. + * + * @author Michael Reiche */ public class Util { - /** - * Waits and sleeps for a little bit of time until the given condition is met. - * - *

Sleeps 1ms between "false" invocations. It will wait at most one minute to prevent hanging forever in case - * the condition never becomes true. - * - * @param supplier return true once it should stop waiting. - */ - public static void waitUntilCondition(final BooleanSupplier supplier) { - waitUntilCondition(supplier, Duration.ofMinutes(1)); - } + /** + * Waits and sleeps for a little bit of time until the given condition is met. + *

+ * Sleeps 1ms between "false" invocations. It will wait at most one minute to prevent hanging forever in case the + * condition never becomes true. + * + * @param supplier return true once it should stop waiting. + */ + public static void waitUntilCondition(final BooleanSupplier supplier) { + waitUntilCondition(supplier, Duration.ofMinutes(1)); + } + + public static void waitUntilCondition(final BooleanSupplier supplier, Duration atMost) { + with().pollInterval(Duration.ofMillis(1)).await().atMost(atMost).until(supplier::getAsBoolean); + } + + public static void waitUntilCondition(final BooleanSupplier supplier, Duration atMost, Duration delay) { + with().pollInterval(delay).await().atMost(atMost).until(supplier::getAsBoolean); + } - public static void waitUntilCondition(final BooleanSupplier supplier, Duration atMost) { - with().pollInterval(Duration.ofMillis(1)).await().atMost(atMost).until(supplier::getAsBoolean); - } + public static void waitUntilThrows(final Class clazz, final Supplier supplier) { + with().pollInterval(Duration.ofMillis(1)).await().atMost(Duration.ofMinutes(1)).until(() -> { + try { + supplier.get(); + } catch (final Exception ex) { + return ex.getClass().isAssignableFrom(clazz); + } + return false; + }); + } - public static void waitUntilCondition(final BooleanSupplier supplier, Duration atMost, Duration delay) { - with().pollInterval(delay).await().atMost(atMost).until(supplier::getAsBoolean); - } + /** + * Returns true if a thread with the given name is currently running. + * + * @param name the name of the thread. + * @return true if running, false otherwise. + */ + public static boolean threadRunning(final String name) { + for (Thread t : Thread.getAllStackTraces().keySet()) { + if (t.getName().equalsIgnoreCase(name)) { + return true; + } + } + return false; + } - public static void waitUntilThrows(final Class clazz, final Supplier supplier) { - with() - .pollInterval(Duration.ofMillis(1)) - .await() - .atMost(Duration.ofMinutes(1)) - .until(() -> { - try { - supplier.get(); - } catch (final Exception ex) { - return ex.getClass().isAssignableFrom(clazz); - } - return false; - }); - } + /** + * Reads a file from the resources folder (in the same path as the requesting test class). + *

+ * The class will be automatically loaded relative to the namespace and converted to a string. + *

+ * + * @param filename the filename of the resource. + * @param clazz the reference class. + * @return the loaded string. + */ + public static String readResource(final String filename, final Class clazz) { + String path = "/" + clazz.getPackage().getName().replace(".", "/") + "/" + filename; + InputStream stream = clazz.getResourceAsStream(path); + java.util.Scanner s = new java.util.Scanner(stream, UTF_8.name()).useDelimiter("\\A"); + return s.hasNext() ? s.next() : ""; + } - /** - * Returns true if a thread with the given name is currently running. - * - * @param name the name of the thread. - * @return true if running, false otherwise. - */ - public static boolean threadRunning(final String name) { - for (Thread t : Thread.getAllStackTraces().keySet()) { - if (t.getName().equalsIgnoreCase(name)) { - return true; - } - } - return false; - } + public static Pair, List> comprises(Iterable source, T... airlines) { + List unexpected = new LinkedList<>(); + List missing = new LinkedList(); + source.forEach(unexpected::add); + for (T t : airlines) { + if (!unexpected.contains(t)) { + missing.add(t); + } else { + unexpected.remove(t); + } + } + if (unexpected.isEmpty() && missing.isEmpty()) { + return null; + } else { + return Pair.of(unexpected, missing); + } + } - /** - * Reads a file from the resources folder (in the same path as the requesting test class). - * - *

The class will be automatically loaded relative to the namespace and converted - * to a string. - * - * @param filename the filename of the resource. - * @param clazz the reference class. - * @return the loaded string. - */ - public static String readResource(final String filename, final Class clazz) { - String path = "/" + clazz.getPackage().getName().replace(".", "/") + "/" + filename; - InputStream stream = clazz.getResourceAsStream(path); - java.util.Scanner s = new java.util.Scanner(stream, UTF_8.name()).useDelimiter("\\A"); - return s.hasNext() ? s.next() : ""; - } + public static Pair, List> exactly(Optional result, T... airlines) { + List source = new LinkedList<>(); + if (result.isPresent()) { + source.add(result.get()); + } + return comprises(source, airlines); + } + // should return null if items in source match (allAirlines - notAirlines) + public static Pair, List> comprisesNot(Iterable source, T[] allAirlines, T... notAirlines) { + List expected = new LinkedList<>(Arrays.asList(allAirlines)); + expected.removeAll(Arrays.asList(notAirlines)); + List unexpected = new LinkedList<>(); + List missing = new LinkedList(); + source.forEach(unexpected::add);// initially, everything returned is unexpected + for (T t : expected) { + if (!unexpected.contains(t)) { + missing.add(t); // if not returned, then it is missing + } else { + unexpected.remove(t); // if returned, then remove from unexpected + } + } + if (unexpected.isEmpty() && missing.isEmpty()) { + return null; + } else { + return Pair.of(unexpected, missing); + } + } }