Querydsl support. (#1330)

Closes #1288.
This commit is contained in:
Michael Reiche
2022-03-31 08:04:17 -07:00
committed by GitHub
parent 99b969833c
commit c288d053c9
32 changed files with 3264 additions and 145 deletions

55
pom.xml
View File

@@ -22,6 +22,9 @@
<couchbase.osgi>3.2.5</couchbase.osgi>
<springdata.commons>2.7.0-SNAPSHOT</springdata.commons>
<java-module-name>spring.data.couchbase</java-module-name>
<apt>1.1.3</apt>
<querydsl>5.0.0</querydsl>
<mysema.querydsl>3.7.4</mysema.querydsl>
</properties>
<dependencyManagement>
@@ -37,6 +40,13 @@
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
@@ -160,9 +170,10 @@
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>${javax-annotation-api}</version>
<scope>test</scope>
</dependency>
<!-- -addmods java.annotations.common -->
<dependency>
<groupId>org.apache.openwebbeans</groupId>
<artifactId>openwebbeans-se</artifactId>
@@ -298,6 +309,48 @@
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>${apt}</version>
<dependencies>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl}</version>
</dependency>
</dependencies>
<executions>
<execution>
<phase>generate-test-sources</phase>
<goals>
<goal>test-process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-test-sources</outputDirectory>
<processor>org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<phase>generate-test-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>target/generated-test-sources</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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<Q extends AbstractCouchbaseQueryDSL<Q>> implements SimpleQuery<Q> {
private final CouchbaseDocumentSerializer serializer;
private final QueryMixin<Q> 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<Q>(query, new DefaultQueryMetadata(), false);
}
/**
* mongodb uses createQuery(Predicate filter) where the serializer creates the 'query' <br>
* and then uses the result to create a BasicQuery with queryObject = result <br>
* Couchbase Query has a 'criteria' which is a <br>
* List<QueryCriteriaDefinition> criteria <br>
* so we could create a List&lt;QueryCriteriaDefinition&gt; or an uber QueryCriteria that combines <br>
* 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 <T> JoinBuilder<Q, T> join(Path<T> ref, Path<T> target) {
// return new JoinBuilder(this.queryMixin, ref, target);
// }
// public <T> JoinBuilder<Q, T> join(CollectionPathBase<?, T, ?> ref, Path<T> target) {
// return new JoinBuilder(this.queryMixin, ref, target);
// }
// public <T> AnyEmbeddedBuilder<Q> anyEmbedded(Path<? extends Collection<T>> collection, Path<T> 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<Expression<?>, Predicate> predicates = new HashMap();
List<JoinExpression> 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<? extends Object> 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<Predicate> predicates) {
return predicates != null ? ExpressionUtils.allOf(predicates) : null;
}
protected abstract List<Object> 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 <T> Q set(ParamExpression<T> param, T value) {
return this.queryMixin.set(param, value);
}
protected Map<String, String> createProjection(Expression<?> projection) {
if (projection instanceof FactoryExpression) {
Map<String, String> 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<Q> 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() {}
}
}

View File

@@ -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<Object, Void> {
boolean workInProgress = true;
public Object handle(Expression<?> expression) {
return expression.accept(this, null);
}
public Sort toSort(List<OrderSpecifier<?>> 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<? extends Enum<?>> expectedExpr = (Constant<? extends Enum<?>>) 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<? extends Collection<?>>) 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<? extends Collection<?>>) 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<Object> list = new ArrayList<Object>(2);
// list.add(asDocument(asDBKey(expr, 0), new ArrayList<Object>()));
// 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<Object> list = new ArrayList<Object>();
for (Map.Entry<String, Object> entry : arg.entrySet()) {
if (entry.getKey().equals("$or")) {
list.add(asDocument("$nor", entry.getValue()));
} else if (entry.getKey().equals("$and")) {
List<Object> list2 = new ArrayList<Object>();
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<Object> list2 = new ArrayList<Object>();
for (Map.Entry<String, Object> 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;
}
}

View File

@@ -105,6 +105,25 @@ public abstract class AbstractCouchbaseConverter implements CouchbaseConverter,
}
/* 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);

View File

@@ -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 @@ import org.springframework.data.util.TypeInformation;
*
* @author Michael Nitschinger
* @author Simon Baslé
* @author Michael Reiche
*/
public interface CouchbaseConverter
extends EntityConverter<CouchbasePersistentEntity<?>, 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);
}

View File

@@ -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 @@ import org.springframework.util.StringUtils;
*
* @author Michael Nitschinger
* @author Mark Paluch
* @author Michael Reiche
*/
public class BasicCouchbasePersistentEntity<T> extends BasicPersistentEntity<T, CouchbasePersistentProperty>
implements CouchbasePersistentEntity<T>, EnvironmentAware {
@@ -164,4 +165,14 @@ public class BasicCouchbasePersistentEntity<T> extends BasicPersistentEntity<T,
return annotation == null ? false : annotation.touchOnRead() && getExpiry() > 0;
}
@Override
public boolean hasTextScoreProperty() {
return false;
}
@Override
public CouchbasePersistentProperty getTextScoreProperty() {
return null;
}
}

View File

@@ -24,6 +24,7 @@ import org.springframework.data.mapping.PersistentEntity;
* Represents an entity that can be persisted which contains 0 or more properties.
*
* @author Michael Nitschinger
* @author Michael Reiche
*/
public interface CouchbasePersistentEntity<T> extends PersistentEntity<T, CouchbasePersistentProperty> {
@@ -61,4 +62,7 @@ public interface CouchbasePersistentEntity<T> extends PersistentEntity<T, Couchb
*/
boolean isTouchOnRead();
boolean hasTextScoreProperty();
CouchbasePersistentProperty getTextScoreProperty();
}

View File

@@ -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.
@@ -18,6 +18,12 @@ package org.springframework.data.couchbase.core.query;
import java.io.Serializable;
import java.util.Iterator;
/**
* A N1QL Query Expression
*
* @author Michael Nitschinger
* @author Michael Reiche
*/
public class N1QLExpression {
private static final N1QLExpression NULL_INSTANCE = new N1QLExpression("NULL");
private static final N1QLExpression TRUE_INSTANCE = new N1QLExpression("TRUE");
@@ -90,6 +96,15 @@ public class N1QLExpression {
return NULL_INSTANCE;
}
/**
* Returns an expression representing WRAPPER.
*
* @return an expression representing WRAPPER.
*/
public static N1QLExpression WRAPPER() {
return EMPTY_INSTANCE;
}
/**
* Returns an expression representing MISSING.
*

View File

@@ -15,7 +15,10 @@
*/
package org.springframework.data.couchbase.core.query;
import static org.springframework.util.Assert.notNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -53,7 +56,7 @@ public class Query {
private int limit;
private boolean distinct;
private String[] distinctFields;
private Sort sort = Sort.unsorted();
protected Sort sort = Sort.unsorted();
private QueryScanConsistency queryScanConsistency;
private Meta meta;
@@ -66,6 +69,19 @@ public class Query {
addCriteria(criteriaDefinition);
}
public Query(Query that) {
Assert.notNull(that, "source query cannot be null");
this.criteria.addAll(that.criteria);
this.parameters = that.parameters;
this.skip = that.skip;
this.limit = that.limit;
this.distinct = that.distinct;
this.distinctFields = that.distinctFields;
this.sort = that.sort;
this.queryScanConsistency = that.queryScanConsistency;
this.meta = that.meta;
};
public static Query query(QueryCriteriaDefinition criteriaDefinition) {
return new Query(criteriaDefinition);
}
@@ -75,6 +91,10 @@ public class Query {
return this;
}
protected List<QueryCriteriaDefinition> 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 class Query {
* @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 class Query {
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 class Query {
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;
}
}

View File

@@ -17,7 +17,6 @@ package org.springframework.data.couchbase.core.query;
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 @@ import com.couchbase.client.java.json.JsonValue;
*/
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<QueryCriteria> criteriaChain;
private LinkedList<QueryCriteria> 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<QueryCriteria> chain, N1QLExpression key, Object[] value, ChainOperator chainOperator) {
public QueryCriteria(LinkedList<QueryCriteria> chain, N1QLExpression key, Object[] value,
ChainOperator chainOperator) {
this(chain, key, value, chainOperator, null, null);
}
QueryCriteria(List<QueryCriteria> chain, N1QLExpression key, Object value, ChainOperator chainOperator) {
QueryCriteria(LinkedList<QueryCriteria> chain, N1QLExpression key, Object value, ChainOperator chainOperator) {
this(chain, key, new Object[] { value }, chainOperator, null, null);
}
QueryCriteria(List<QueryCriteria> chain, N1QLExpression key, Object[] value, ChainOperator chainOperator,
QueryCriteria(LinkedList<QueryCriteria> 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 class QueryCriteria implements QueryCriteriaDefinition {
* 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 class QueryCriteria implements QueryCriteriaDefinition {
}
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 class QueryCriteria implements QueryCriteriaDefinition {
}
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 class QueryCriteria implements QueryCriteriaDefinition {
}
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 class QueryCriteria implements QueryCriteriaDefinition {
}
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 class QueryCriteria implements QueryCriteriaDefinition {
}
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 class QueryCriteria implements QueryCriteriaDefinition {
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 @@ public class QueryCriteria implements QueryCriteriaDefinition {
}
}
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 @@ public class QueryCriteria implements QueryCriteriaDefinition {
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();
}
}

View File

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

View File

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

View File

@@ -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<String, String> 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<String, String> projectionFields) {
super(query);
Assert.notNull(projectionFields, "Field document must not be null");
this.projectionFields = projectionFields;
}
public BasicQuery(QueryCriteriaDefinition criteria, Map<String, String> 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<String, String> 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;
}
}

View File

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

View File

@@ -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.PartTreeCouchbaseQuer
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 class CouchbaseRepositoryFactory extends RepositoryFactorySupport {
this.couchbaseOperationsMapping = couchbaseOperationsMapping;
this.crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor();
mappingContext = this.couchbaseOperationsMapping.getMappingContext();
operations = this.couchbaseOperationsMapping.getDefault();
addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor);
}
@@ -168,4 +175,44 @@ public class CouchbaseRepositoryFactory extends RepositoryFactorySupport {
}
}
/*
* (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.
* <p>
* 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();
}
}

View File

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

View File

@@ -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<P, T> implements FluentQuery.FetchableFluentQuery<T> {
private final P predicate;
private final Sort sort;
private final Class<T> resultType;
private final List<String> fieldsToInclude;
FetchableFluentQuerySupport(P predicate, Sort sort, Class<T> resultType, List<String> fieldsToInclude) {
this.predicate = predicate;
this.sort = sort;
this.resultType = resultType;
this.fieldsToInclude = fieldsToInclude;
}
protected abstract <R> FetchableFluentQuerySupport<P, R> create(P predicate, Sort sort, Class<R> resultType,
List<String> 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<T> all();
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#page(org.springframework.data.domain.Pageable)
*/
public abstract Page<T> page(Pageable pageable);
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#stream()
*/
public abstract Stream<T> 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<T> getResultType() {
return resultType;
}
List<String> getFieldsToInclude() {
return fieldsToInclude;
}
}

View File

@@ -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<T> extends QuerydslPredicateExecutorSupport<T>
implements QuerydslPredicateExecutor<T> {
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<T, ?> 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<T, ?> 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<T> 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<T> 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<T> 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<T> 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<T> 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<T> findAll(Predicate predicate, Pageable pageable) {
Assert.notNull(predicate, "Predicate must not be null!");
Assert.notNull(pageable, "Pageable must not be null!");
SpringDataCouchbaseQuery<T> 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 <S extends T, R> R findBy(Predicate predicate,
Function<FluentQuery.FetchableFluentQuery<S>, 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<S>) typeInformation().getJavaType()));
}
/**
* Creates a {@link SpringDataCouchbaseQuery} for the given {@link Predicate}.
*
* @param predicate
* @return
*/
private SpringDataCouchbaseQuery<T> createQueryFor(Predicate predicate) {
return createQuery().where(predicate);
}
/**
* Creates a {@link SpringDataCouchbaseQuery}.
*
* @return
*/
private SpringDataCouchbaseQuery<T> createQuery() {
return new SpringDataCouchbaseQuery<>(couchbaseOperations, typeInformation().getJavaType());
}
/**
* Applies the given {@link Pageable} to the given {@link SpringDataCouchbaseQuery}.
*
* @param query
* @param pageable
* @return
*/
private SpringDataCouchbaseQuery<T> applyPagination(SpringDataCouchbaseQuery<T> 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<T> applySorting(SpringDataCouchbaseQuery<T> 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<T> extends FetchableFluentQuerySupport<Predicate, T> {
FluentQuerydsl(Predicate predicate, Class<T> resultType) {
this(predicate, Sort.unsorted(), resultType, Collections.emptyList());
}
FluentQuerydsl(Predicate predicate, Sort sort, Class<T> resultType, List<String> fieldsToInclude) {
super(predicate, sort, resultType, fieldsToInclude);
}
@Override
protected <R> FluentQuerydsl<R> create(Predicate predicate, Sort sort, Class<R> resultType,
List<String> 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<T> all() {
return createQuery().fetch();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery#page(org.springframework.data.domain.Pageable)
*/
@Override
public Page<T> 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<T> 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<T> createQuery() {
return new SpringDataCouchbaseQuery<>(couchbaseOperations, typeInformation().getJavaType(), getResultType(),
"collection", this::customize).where(getPredicate());
}
private void customize(BasicQuery query) {
List<String> fieldsToInclude = getFieldsToInclude();
if (!fieldsToInclude.isEmpty()) {
Map<String, String> fields = new HashMap();
fieldsToInclude.forEach(field -> fields.put(field, field));
query.setProjectionFields(fields);
}
if (getSort().isSorted()) {
query.with(getSort());
}
}
}
}

View File

@@ -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<T> {
private final SpringDataCouchbaseSerializer serializer;
private final PathBuilder<T> builder;
private final EntityInformation<T, ?> entityInformation;
QuerydslPredicateExecutorSupport(CouchbaseConverter converter, PathBuilder<T> builder,
EntityInformation<T, ?> entityInformation) {
this.serializer = new SpringDataCouchbaseSerializer(converter);
this.builder = builder;
this.entityInformation = entityInformation;
}
protected static <E> PathBuilder<E> pathBuilderFor(EntityPath<E> path) {
return new PathBuilder<>(path.getType(), path.getMetadata());
}
protected EntityInformation<T, ?> 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<Object> 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<OrderSpecifier<?>> toOrderSpecifiers(Sort sort) {
if (sort instanceof QSort) {
return ((QSort) sort).getOrderSpecifiers();
}
return sort.stream().map(this::toOrder).collect(Collectors.toList());
}
}

View File

@@ -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<T> extends SpringDataCouchbaseQuerySupport<SpringDataCouchbaseQuery<T>>
implements Fetchable<T> {
private final CouchbaseOperations couchbaseOperations;
private final Consumer<BasicQuery> queryCustomizer;
private final ExecutableFindByQueryOperation.ExecutableFindByQuery<T> find;// ExecutableFindOperation.FindWithQuery<T>
// 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<? extends T> 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<? extends T> 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<? extends T> resultType,
String collectionName, Consumer<BasicQuery> queryCustomizer) {
super(new SpringDataCouchbaseSerializer(operations.getConverter()));
Class<T> resultType1 = (Class<T>) resultType;
this.couchbaseOperations = operations;
this.queryCustomizer = queryCustomizer;
this.find = (ExecutableFindByQueryOperation.ExecutableFindByQuery<T>) couchbaseOperations.findByQuery(domainType)
.as(resultType1).inCollection(collectionName);
}
/*
* (non-Javadoc)
* @see com.querydsl.core.Fetchable#iterable()
*/
@Override
public CloseableIterator<T> iterate() {
try {
Stream<T> stream = stream();
Iterator<T> iterator = stream.iterator();
return new CloseableIterator<T>() {
@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<T> 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<T> fetch() {
try {
return find.matching(createQuery()).all();
} catch (RuntimeException e) {
return handleException(e, Collections.emptyList());
}
}
/**
* Fetch a {@link Page}.
*
* @param pageable
* @return
*/
public Page<T> fetchPage(Pageable pageable) {
try {
List<T> 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<T> 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<Object> 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<OrderSpecifier<?>> orderBy) {
Map<String, String> 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<String,String> createProjection(Expression<?> projection);
// @Override
// protected CouchbaseDocument createSort(List<OrderSpecifier<?>> 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<Object> 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> T handleException(RuntimeException e, T defaultValue) {
if (e.getClass().getName().endsWith("$NoResults")) {
return defaultValue;
}
throw e;
}
}

View File

@@ -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<Q extends SpringDataCouchbaseQuerySupport<Q>>
extends AbstractCouchbaseQueryDSL<Q> {
private final QueryMixin<Q> 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<Q>) fieldAccessor.getPropertyValue("queryMixin");
}
/**
* Returns the representation of the query. <br />
* The following query
*
* <pre class="code">
*
* where(p.lastname.eq("Matthews")).orderBy(p.firstname.asc()).offset(1).limit(5);
* </pre>
*
* results in
*
* <pre class="code">
*
* find({"lastname" : "Matthews"}).sort({"firstname" : 1}).skip(1).limit(5)
* </pre>
*
* 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<String, String> 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<OrderSpecifier<?>> orderSpecifiers) {
return null; // TODO serializer.toSort(orderSpecifiers);
}
}

View File

@@ -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<PathType> PATH_TYPES;
static {
Set<PathType> pathTypes = new HashSet<>();
pathTypes.add(PathType.VARIABLE);
pathTypes.add(PathType.PROPERTY);
PATH_TYPES = Collections.unmodifiableSet(pathTypes);
}
private final CouchbaseConverter converter;
private final MappingContext<? extends CouchbasePersistentEntity<?>, 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;
}
}

View File

@@ -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 @@ public class BasicCouchbasePersistentEntityTests {
@Configuration
static class Config {}
public static class SimpleDocument {}
class SimpleDocument {}
@Document(expiry = 10)
public static class SimpleDocumentWithExpiry {}
class SimpleDocumentWithExpiry {}
@Document(expiry = 10, touchOnRead = true)
public static class SimpleDocumentWithTouchOnRead {}
class SimpleDocumentWithTouchOnRead {}
/**
* Simple POJO to test default expiry.
*/
@Document
private class DefaultExpiry {}
class DefaultExpiry {}
/**
* Simple POJO to test default expiry unit.
*/
@Document(expiry = 78)
private class DefaultExpiryUnit {}
class DefaultExpiryUnit {}
/**
* Simple POJO to test limit expiry.
*/
@Document(expiry = 30, expiryUnit = TimeUnit.DAYS)
private class LimitDaysExpiry {}
class LimitDaysExpiry {}
/**
* Simple POJO to test larger than 30 days expiry.
*/
@Document(expiry = 31, expiryUnit = TimeUnit.DAYS)
public class OverLimitDaysExpiry {}
class OverLimitDaysExpiry {}
/**
* Simple POJO to test larger than 30 days expiry defined as an expression
*/
@Document(expiryExpression = "${document.expiry.larger.than.30days:31}", expiryUnit = TimeUnit.DAYS)
public class OverLimitDaysExpiryExpression {}
class OverLimitDaysExpiryExpression {}
/**
* Simple POJO to test larger than 30 days expiry, when expressed in default time unit (SECONDS).
*/
@Document(expiry = 31 * 24 * 60 * 60)
public class OverLimitSecondsExpiry {}
class OverLimitSecondsExpiry {}
/**
* Simple POJO to test constant expiry expression
*/
@Document(expiryExpression = "10")
private class ConstantExpiryExpression {}
class ConstantExpiryExpression {}
/**
* Simple POJO to test valid expiry expression by resolving simple property from environment
*/
@Document(expiryExpression = "${valid.document.expiry}")
private class ExpiryWithValidExpression {}
class ExpiryWithValidExpression {}
/**
* Simple POJO to test invalid expiry expression
*/
@Document(expiryExpression = "${invalid.document.expiry}")
private class ExpiryWithInvalidExpression {}
class ExpiryWithInvalidExpression {}
/**
* Simple POJO to test expiry expression logic failure to resolve property placeholder
*/
@Document(expiryExpression = "${missing.expiry}")
private class ExpiryWithMissingProperty {}
class ExpiryWithMissingProperty {}
/**
* Simple POJO to test that expiry and expiry expression cannot be used simultaneously
*/
@Document(expiry = 10, expiryExpression = "10")
private class ExpiryAndExpression {}
class ExpiryAndExpression {}
}

View File

@@ -32,6 +32,7 @@ import com.couchbase.client.java.json.JsonArray;
/**
* @author Mauro Monti
* @author Michael Reiche
*/
class QueryCriteriaTests {
@@ -69,7 +70,7 @@ class QueryCriteriaTests {
void testNestedAndCriteria() {
QueryCriteria c = where(i("name")).is("Bubba").and(where(i("age")).gt(12).or(i("country")).is("Austria"));
JsonArray parameters = JsonArray.create();
assertEquals("`name` = $1 and (`age` > $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 @@ class QueryCriteriaTests {
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 @@ class QueryCriteriaTests {
@Test
void testFalse() {
QueryCriteria c = where(i("name")).FALSE();
assertEquals("not( (`name`) )", c.export());
assertEquals("not(`name`)", c.export());
}
@Test

View File

@@ -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 @@ import org.springframework.data.couchbase.core.mapping.Document;
@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 class Airline extends ComparableEntity {
return name;
}
public String getHqCountry() {
return hqCountry;
}
}

View File

@@ -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<Airline, String> {
public interface AirlineRepository extends PagingAndSortingRepository<Airline, String>,
QuerydslPredicateExecutor<Airline>, DynamicProxyable<AirlineRepository> {
@Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (name = $1)")
List<User> getByName(@Param("airline_name")String airlineName);
List<User> getByName(@Param("airline_name") String airlineName);
}

View File

@@ -115,7 +115,7 @@ public class CouchbaseRepositoryKeyValueIntegrationTests extends ClusterAwareInt
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.

View File

@@ -67,6 +67,7 @@ import org.springframework.data.couchbase.core.mapping.event.ValidatingCouchbase
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;

View File

@@ -76,7 +76,7 @@ public class ReactiveCouchbaseRepositoryKeyValueIntegrationTests extends Cluster
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.

View File

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

View File

@@ -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.Optional;
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.data.repository.query.QueryMethodEvaluationContextPro
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 @@ class StringN1qlQueryCreatorTests extends ClusterAwareIntegrationTests {
@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 @@ class StringN1qlQueryCreatorTests extends ClusterAwareIntegrationTests {
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<Airline> al = q.one();
assertEquals(airline.toString(), al.get().toString());

View File

@@ -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 java.io.InputStream;
import java.time.Duration;
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 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 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.
*
* <p>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.
* <p>
* 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) {
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, Duration delay) {
with().pollInterval(delay).await().atMost(atMost).until(supplier::getAsBoolean);
}
public static void waitUntilThrows(final Class<? extends Exception> clazz, final Supplier<Object> 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 waitUntilThrows(final Class<? extends Exception> clazz, final Supplier<Object> 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;
});
}
/**
* 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;
}
/**
* 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;
}
/**
* Reads a file from the resources folder (in the same path as the requesting test class).
*
* <p>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() : "";
}
/**
* Reads a file from the resources folder (in the same path as the requesting test class).
* <p>
* The class will be automatically loaded relative to the namespace and converted to a string.
* </p>
*
* @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 <T> Pair<List<T>, List<T>> comprises(Iterable<T> source, T... airlines) {
List<T> unexpected = new LinkedList<>();
List<T> missing = new LinkedList<T>();
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);
}
}
public static <T> Pair<List<T>, List<T>> exactly(Optional<T> result, T... airlines) {
List<T> 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 <T> Pair<List<T>, List<T>> comprisesNot(Iterable<T> source, T[] allAirlines, T... notAirlines) {
List<T> expected = new LinkedList<>(Arrays.asList(allAirlines));
expected.removeAll(Arrays.asList(notAirlines));
List<T> unexpected = new LinkedList<>();
List<T> missing = new LinkedList<T>();
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);
}
}
}