diff --git a/pom.xml b/pom.xml index 0ff20c5825..aa17486981 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1848-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 9baccaa905..d9e456b116 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1848-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index 47a5b7aba7..cb11335699 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1848-SNAPSHOT ../pom.xml @@ -50,7 +50,7 @@ org.springframework.data spring-data-mongodb - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1848-SNAPSHOT diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index e5c865ea08..3ef74f0491 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1848-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index b86dc2808c..fdd6ad4a86 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1848-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java index ef6c1423c9..15a9ae691b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java @@ -62,7 +62,10 @@ public interface MongoDbFactory extends CodecRegistryProvider, MongoSessionProvi * Get the legacy database entry point. Please consider {@link #getDb()} instead. * * @return + * @deprecated since 2.1, use {@link #getDb()}. This method will be removed with a future version as it works only + * with the legacy MongoDB driver. */ + @Deprecated DB getLegacyDb(); /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java index 6b2202db43..73e9ecdb36 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java @@ -22,15 +22,13 @@ import java.util.UUID; import java.util.regex.Pattern; -import org.bson.BsonObjectId; +import org.bson.*; import org.bson.types.Binary; import org.bson.types.CodeWScope; import org.bson.types.CodeWithScope; import org.bson.types.Decimal128; import org.bson.types.ObjectId; import org.springframework.data.mapping.model.SimpleTypeHolder; -import org.springframework.data.mongodb.util.MongoClientVersion; -import org.springframework.util.ClassUtils; import com.mongodb.DBRef; @@ -62,6 +60,24 @@ public abstract class MongoSimpleTypes { simpleTypes.add(Binary.class); simpleTypes.add(UUID.class); simpleTypes.add(Decimal128.class); + + simpleTypes.add(BsonBinary.class); + simpleTypes.add(BsonBoolean.class); + simpleTypes.add(BsonDateTime.class); + simpleTypes.add(BsonDbPointer.class); + simpleTypes.add(BsonDecimal128.class); + simpleTypes.add(BsonDocument.class); + simpleTypes.add(BsonDocument.class); + simpleTypes.add(BsonDouble.class); + simpleTypes.add(BsonInt32.class); + simpleTypes.add(BsonInt64.class); + simpleTypes.add(BsonJavaScript.class); + simpleTypes.add(BsonJavaScriptWithScope.class); + simpleTypes.add(BsonObjectId.class); + simpleTypes.add(BsonRegularExpression.class); + simpleTypes.add(BsonString.class); + simpleTypes.add(BsonTimestamp.class); + MONGO_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java new file mode 100644 index 0000000000..048233d5ba --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java @@ -0,0 +1,437 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.support; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Pattern; + +import org.bson.BsonJavaScript; +import org.bson.BsonRegularExpression; +import org.bson.Document; +import org.bson.types.ObjectId; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import com.mongodb.DBRef; +import com.querydsl.core.types.*; +import com.querydsl.mongodb.MongodbOps; + +/** + *

+ * Serializes the given Querydsl query to a Document query for MongoDB. + *

+ *

+ * Original implementation source {@link com.querydsl.mongodb.MongodbSerializer} by {@literal The Querydsl Team} + * (http://www.querydsl.com/team) licensed under the Apache License, Version + * 2.0. + *

+ * Modified to use {@link Document} instead of {@link com.mongodb.DBObject}, updated nullable types and code format. Use + * Bson specific types and add {@link QuerydslMongoOps#NO_MATCH}. + * + * @author laimw + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.1 + */ +abstract class MongodbDocumentSerializer implements Visitor { + + @Nullable + Object handle(Expression expression) { + return expression.accept(this, null); + } + + /** + * Create the MongoDB specific query document. + * + * @param predicate must not be {@literal null}. + * @return empty {@link Document} by default. + */ + Document toQuery(Predicate predicate) { + + Object value = handle(predicate); + + if (value == null) { + return new Document(); + } + + Assert.isInstanceOf(Document.class, value, + () -> String.format("Invalid type. Expected Document but found %s", value.getClass())); + + return (Document) value; + } + + /** + * Create the MongoDB specific sort document. + * + * @param orderBys must not be {@literal null}. + * @return empty {@link Document} by default. + */ + Document toSort(List> orderBys) { + + Document sort = new Document(); + + orderBys.forEach(orderSpecifier -> { + + Object key = orderSpecifier.getTarget().accept(this, null); + + Assert.notNull(key, () -> String.format("Mapped sort key for %s must not be null!", orderSpecifier)); + sort.append(key.toString(), orderSpecifier.getOrder() == Order.ASC ? 1 : -1); + }); + + return sort; + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.types.Visitor#visit(com.querydsl.core.types.Constant, java.lang.Void) + */ + @Override + public Object visit(Constant expr, Void context) { + + if (!Enum.class.isAssignableFrom(expr.getType())) { + return expr.getConstant(); + } + + @SuppressWarnings("unchecked") // Guarded by previous check + Constant> expectedExpr = (Constant>) expr; + return expectedExpr.getConstant().name(); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.types.Visitor#visit(com.querydsl.core.types.TemplateExpression, java.lang.Void) + */ + @Override + public Object visit(TemplateExpression expr, Void context) { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.types.Visitor#visit(com.querydsl.core.types.FactoryExpression, java.lang.Void) + */ + @Override + public Object visit(FactoryExpression expr, Void context) { + throw new UnsupportedOperationException(); + } + + protected String asDBKey(Operation expr, int index) { + + String key = (String) asDBValue(expr, index); + + Assert.hasText(key, () -> String.format("Mapped key must not be null nor empty for expression %s.", expr)); + return key; + } + + @Nullable + protected Object asDBValue(Operation expr, int index) { + return expr.getArg(index).accept(this, null); + } + + private String regexValue(Operation expr, int index) { + + Object value = expr.getArg(index).accept(this, null); + + Assert.notNull(value, () -> String.format("Regex for %s must not be null.", expr)); + return Pattern.quote(value.toString()); + } + + protected Document asDocument(String key, @Nullable Object value) { + return new Document(key, value); + } + + @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) { + return asDocument(asDBKey(lhs, 0), asDocument("$size", 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)); + } + } else if (op == Ops.STRING_IS_EMPTY) { + return asDocument(asDBKey(expr, 0), ""); + } else if (op == Ops.AND) { + + Map lhs = (Map) handle(expr.getArg(0)); + Map rhs = (Map) handle(expr.getArg(1)); + + LinkedHashSet> lhs2 = new LinkedHashSet<>(lhs.entrySet()); + lhs2.retainAll(rhs.entrySet()); + + if (lhs2.isEmpty()) { + lhs.putAll(rhs); + return lhs; + } else { + List list = new ArrayList<>(2); + list.add(handle(expr.getArg(0))); + list.add(handle(expr.getArg(1))); + return asDocument("$and", list); + } + + } 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 { + Document arg = (Document) handle(expr.getArg(0)); + return negate(arg); + } + + } else if (op == Ops.OR) { + + List list = new ArrayList<>(2); + list.add(handle(expr.getArg(0))); + list.add(handle(expr.getArg(1))); + return asDocument("$or", list); + + } 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))); + + } else if (op == Ops.STARTS_WITH) { + return asDocument(asDBKey(expr, 0), new BsonRegularExpression("^" + regexValue(expr, 1))); + } else if (op == Ops.STARTS_WITH_IC) { + return asDocument(asDBKey(expr, 0), new BsonRegularExpression("^" + regexValue(expr, 1), "i")); + } else if (op == Ops.ENDS_WITH) { + return asDocument(asDBKey(expr, 0), new BsonRegularExpression(regexValue(expr, 1) + "$")); + } else if (op == Ops.ENDS_WITH_IC) { + return asDocument(asDBKey(expr, 0), new BsonRegularExpression(regexValue(expr, 1) + "$", "i")); + } else if (op == Ops.EQ_IGNORE_CASE) { + return asDocument(asDBKey(expr, 0), new BsonRegularExpression("^" + regexValue(expr, 1) + "$", "i")); + } else if (op == Ops.STRING_CONTAINS) { + return asDocument(asDBKey(expr, 0), new BsonRegularExpression(".*" + regexValue(expr, 1) + ".*")); + } else if (op == Ops.STRING_CONTAINS_IC) { + return asDocument(asDBKey(expr, 0), new BsonRegularExpression(".*" + regexValue(expr, 1) + ".*", "i")); + } else if (op == Ops.MATCHES) { + return asDocument(asDBKey(expr, 0), new BsonRegularExpression(asDBValue(expr, 1).toString())); + } else if (op == Ops.MATCHES_IC) { + return asDocument(asDBKey(expr, 0), new BsonRegularExpression(asDBValue(expr, 1).toString(), "i")); + } else if (op == Ops.LIKE) { + + String regex = ExpressionUtils.likeToRegex((Expression) expr.getArg(1)).toString(); + return asDocument(asDBKey(expr, 0), new BsonRegularExpression(regex)); + } else if (op == Ops.BETWEEN) { + + Document value = new Document("$gte", asDBValue(expr, 1)); + value.append("$lte", asDBValue(expr, 2)); + return asDocument(asDBKey(expr, 0), value); + } else if (op == Ops.IN) { + + int constIndex = 0; + int exprIndex = 1; + if (expr.getArg(1) instanceof Constant) { + constIndex = 1; + exprIndex = 0; + } + if (Collection.class.isAssignableFrom(expr.getArg(constIndex).getType())) { + @SuppressWarnings("unchecked") // guarded by previous check + Collection values = ((Constant>) expr.getArg(constIndex)).getConstant(); + return asDocument(asDBKey(expr, exprIndex), asDocument("$in", values)); + } else { + Path path = (Path) expr.getArg(exprIndex); + Constant constant = (Constant) expr.getArg(constIndex); + return asDocument(asDBKey(expr, exprIndex), convert(path, constant)); + } + } else if (op == Ops.NOT_IN) { + + int constIndex = 0; + int exprIndex = 1; + if (expr.getArg(1) instanceof Constant) { + + constIndex = 1; + exprIndex = 0; + } + if (Collection.class.isAssignableFrom(expr.getArg(constIndex).getType())) { + + @SuppressWarnings("unchecked") // guarded by previous check + Collection values = ((Constant>) expr.getArg(constIndex)).getConstant(); + return asDocument(asDBKey(expr, exprIndex), asDocument("$nin", values)); + } else { + + Path path = (Path) expr.getArg(exprIndex); + Constant constant = (Constant) expr.getArg(constIndex); + return asDocument(asDBKey(expr, exprIndex), asDocument("$ne", convert(path, constant))); + } + } else if (op == Ops.COL_IS_EMPTY) { + + List list = new ArrayList<>(2); + list.add(asDocument(asDBKey(expr, 0), new ArrayList())); + list.add(asDocument(asDBKey(expr, 0), asDocument("$exists", false))); + return asDocument("$or", list); + } else if (op == Ops.LT) { + return asDocument(asDBKey(expr, 0), asDocument("$lt", asDBValue(expr, 1))); + } else if (op == Ops.GT) { + return asDocument(asDBKey(expr, 0), asDocument("$gt", asDBValue(expr, 1))); + } else if (op == Ops.LOE) { + return asDocument(asDBKey(expr, 0), asDocument("$lte", asDBValue(expr, 1))); + } else if (op == Ops.GOE) { + return asDocument(asDBKey(expr, 0), asDocument("$gte", asDBValue(expr, 1))); + } else if (op == Ops.IS_NULL) { + return asDocument(asDBKey(expr, 0), asDocument("$exists", false)); + } else if (op == Ops.IS_NOT_NULL) { + return asDocument(asDBKey(expr, 0), asDocument("$exists", true)); + } else if (op == Ops.CONTAINS_KEY) { + + Path path = (Path) expr.getArg(0); + Expression key = expr.getArg(1); + return asDocument(visit(path, context) + "." + key.toString(), asDocument("$exists", true)); + } else if (op == MongodbOps.NEAR) { + return asDocument(asDBKey(expr, 0), asDocument("$near", asDBValue(expr, 1))); + } else if (op == MongodbOps.NEAR_SPHERE) { + return asDocument(asDBKey(expr, 0), asDocument("$nearSphere", asDBValue(expr, 1))); + } else if (op == MongodbOps.ELEM_MATCH) { + return asDocument(asDBKey(expr, 0), asDocument("$elemMatch", asDBValue(expr, 1))); + } else if (op == QuerydslMongoOps.NO_MATCH) { + return new Document("$where", new BsonJavaScript("function() { return false }")); + } + + throw new UnsupportedOperationException("Illegal operation " + expr); + } + + private Object negate(Document arg) { + + List list = new ArrayList<>(); + for (Map.Entry entry : arg.entrySet()) { + + if (entry.getKey().equals("$or")) { + list.add(asDocument("$nor", entry.getValue())); + } else if (entry.getKey().equals("$and")) { + + List list2 = new ArrayList<>(); + for (Object o : ((Collection) entry.getValue())) { + list2.add(negate((Document) o)); + } + list.add(asDocument("$or", list2)); + } else if (entry.getValue() instanceof Pattern || entry.getValue() instanceof BsonRegularExpression) { + list.add(asDocument(entry.getKey(), asDocument("$not", entry.getValue()))); + } else if (entry.getValue() instanceof Document) { + list.add(negate(entry.getKey(), (Document) 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, Document value) { + + if (value.size() == 1) { + return asDocument(key, asDocument("$not", value)); + } else { + + List list2 = new ArrayList<>(); + for (Map.Entry entry2 : value.entrySet()) { + list2.add(asDocument(key, asDocument("$not", asDocument(entry2.getKey(), entry2.getValue())))); + } + + return asDocument("$or", list2); + } + } + + 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(@Nullable 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(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslAbstractMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslAbstractMongodbQuery.java new file mode 100644 index 0000000000..74ff1789b9 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslAbstractMongodbQuery.java @@ -0,0 +1,202 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.support; + +import java.util.List; + +import org.bson.Document; +import org.springframework.lang.Nullable; + +import com.querydsl.core.DefaultQueryMetadata; +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.FactoryExpression; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.ParamExpression; +import com.querydsl.core.types.Predicate; + +/** + * {@code QuerydslAbstractMongodbQuery} provides a base class for general Querydsl query implementation. + *

+ * Original implementation source {@link com.querydsl.mongodb.AbstractMongodbQuery} by {@literal The Querydsl Team} + * (http://www.querydsl.com/team) licensed under the Apache License, Version + * 2.0. + *

+ * Modified for usage with {@link MongodbDocumentSerializer}. + * + * @param concrete subtype + * @author laimw + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.1 + */ +public abstract class QuerydslAbstractMongodbQuery> + implements SimpleQuery { + + private final MongodbDocumentSerializer serializer; + private final QueryMixin queryMixin; + + /** + * Create a new MongodbQuery instance + * + * @param serializer serializer + */ + @SuppressWarnings("unchecked") + QuerydslAbstractMongodbQuery(MongodbDocumentSerializer serializer) { + + this.queryMixin = new QueryMixin<>((Q) this, new DefaultQueryMetadata(), false); + this.serializer = serializer; + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.SimpleQuery#distinct() + */ + @Override + public Q distinct() { + return queryMixin.distinct(); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.FilteredClause#where(com.querydsl.core.types.Predicate[]) + */ + @Override + public Q where(Predicate... e) { + return queryMixin.where(e); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.SimpleQuery#limit(long) + */ + @Override + public Q limit(long limit) { + return queryMixin.limit(limit); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.SimpleQuery#offset() + */ + @Override + public Q offset(long offset) { + return queryMixin.offset(offset); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.SimpleQuery#restrict(com.querydsl.core.QueryModifiers) + */ + @Override + public Q restrict(QueryModifiers modifiers) { + return queryMixin.restrict(modifiers); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.SimpleQuery#orderBy(com.querydsl.core.types.OrderSpecifier) + */ + @Override + public Q orderBy(OrderSpecifier... o) { + return queryMixin.orderBy(o); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.SimpleQuery#set(com.querydsl.core.types.ParamExpression, Object) + */ + @Override + public Q set(ParamExpression param, T value) { + return queryMixin.set(param, value); + } + + /** + * Compute the actual projection {@link Document} from a given projectionExpression by serializing the contained + * {@link Expression expressions} individually. + * + * @param projectionExpression the computed projection {@link Document}. + * @return never {@literal null}. An empty {@link Document} by default. + * @see MongodbDocumentSerializer#handle(Expression) + */ + protected Document createProjection(@Nullable Expression projectionExpression) { + + if (!(projectionExpression instanceof FactoryExpression)) { + return new Document(); + } + + Document projection = new Document(); + ((FactoryExpression) projectionExpression).getArgs().stream() // + .filter(Expression.class::isInstance) // + .map(Expression.class::cast) // + .map(serializer::handle) // + .forEach(it -> projection.append(it.toString(), 1)); + + return projection; + } + + /** + * Compute the filer {@link Document} from the given {@link Predicate}. + * + * @param predicate can be {@literal null}. + * @return an empty {@link Document} if predicate is {@literal null}. + * @see MongodbDocumentSerializer#toQuery(Predicate) + */ + protected Document createQuery(@Nullable Predicate predicate) { + + if (predicate == null) { + return new Document(); + } + + return serializer.toQuery(predicate); + } + + /** + * Compute the sort {@link Document} from the given list of {@link OrderSpecifier order specifiers}. + * + * @param orderSpecifiers can be {@literal null}. + * @return an empty {@link Document} if predicate is {@literal null}. + * @see MongodbDocumentSerializer#toSort(List) + */ + protected Document createSort(List> orderSpecifiers) { + return serializer.toSort(orderSpecifiers); + } + + /** + * Get the actual {@link QueryMixin} delegate. + * + * @return + */ + QueryMixin getQueryMixin() { + return queryMixin; + } + + /** + * Get the where definition as a Document instance + * + * @return + */ + Document asDocument() { + return createQuery(queryMixin.getMetadata().getWhere()); + } + + @Override + public String toString() { + return asDocument().toString(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslAnyEmbeddedBuilder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslAnyEmbeddedBuilder.java new file mode 100644 index 0000000000..af9380ba2f --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslAnyEmbeddedBuilder.java @@ -0,0 +1,64 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.support; + +import java.util.Collection; + +import com.querydsl.core.support.QueryMixin; +import com.querydsl.core.types.ExpressionUtils; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.Predicate; +import com.querydsl.mongodb.MongodbOps; + +/** + * {@code QuerydslAnyEmbeddedBuilder} is a builder for constraints on embedded objects. + *

+ * Original implementation source {@link com.querydsl.mongodb.AnyEmbeddedBuilder} by {@literal The Querydsl Team} + * (http://www.querydsl.com/team) licensed under the Apache License, Version + * 2.0. + *

+ * Modified for usage with {@link QuerydslAbstractMongodbQuery}. + * + * @param query type + * @author tiwe + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.1 + */ +public class QuerydslAnyEmbeddedBuilder, K> { + + private final QueryMixin queryMixin; + private final Path> collection; + + QuerydslAnyEmbeddedBuilder(QueryMixin queryMixin, Path> collection) { + + this.queryMixin = queryMixin; + this.collection = collection; + } + + /** + * Add the given where conditions. + * + * @param conditions must not be {@literal null}. + * @return the target {@link QueryMixin}. + * @see QueryMixin#where(Predicate) + */ + public Q on(Predicate... conditions) { + + return queryMixin + .where(ExpressionUtils.predicate(MongodbOps.ELEM_MATCH, collection, ExpressionUtils.allOf(conditions))); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslFetchableMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslFetchableMongodbQuery.java new file mode 100644 index 0000000000..e9f1b09865 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslFetchableMongodbQuery.java @@ -0,0 +1,272 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.support; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithProjection; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Nullable; +import org.springframework.util.LinkedMultiValueMap; + +import com.mysema.commons.lang.CloseableIterator; +import com.querydsl.core.Fetchable; +import com.querydsl.core.JoinExpression; +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.ExpressionUtils; +import com.querydsl.core.types.Operation; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.CollectionPathBase; + +/** + * {@link Fetchable} MongoDB query with utilizing {@link MongoOperations} for command execution. + * + * @param result type + * @param concrete subtype + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.1 + */ +abstract class QuerydslFetchableMongodbQuery> + extends QuerydslAbstractMongodbQuery implements Fetchable { + + private final Class entityClass; + private final String collection; + private final MongoOperations mongoOperations; + private final FindWithProjection find; + + QuerydslFetchableMongodbQuery(MongodbDocumentSerializer serializer, Class entityClass, String collection, + MongoOperations mongoOperations) { + + super(serializer); + + this.entityClass = (Class) entityClass; + this.collection = collection; + this.mongoOperations = mongoOperations; + find = mongoOperations.query(this.entityClass).inCollection(collection); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#iterable() + */ + @Override + public CloseableIterator iterate() { + + org.springframework.data.util.CloseableIterator stream = mongoOperations.stream(createQuery(), + entityClass, collection); + + return new CloseableIterator() { + + @Override + public boolean hasNext() { + return stream.hasNext(); + } + + @Override + public K next() { + return stream.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Cannot remove from iterator while streaming data."); + } + + @Override + public void close() { + stream.close(); + } + }; + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetch() + */ + @Override + public List fetch() { + return find.matching(createQuery()).all(); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetchFirst() + */ + @Override + public K fetchFirst() { + return find.matching(createQuery()).firstValue(); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetchOne() + */ + @Override + public K fetchOne() { + return find.matching(createQuery()).oneValue(); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetchResults() + */ + @Override + public QueryResults fetchResults() { + + long total = fetchCount(); + return total > 0L ? new QueryResults<>(fetch(), getQueryMixin().getMetadata().getModifiers(), total) + : QueryResults.emptyResults(); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetchCount() + */ + @Override + public long fetchCount() { + return find.matching(createQuery()).count(); + } + + /** + * Define a join. + * + * @param ref reference + * @param target join target + * @return new instance of {@link QuerydslJoinBuilder}. + */ + public QuerydslJoinBuilder join(Path ref, Path target) { + return new QuerydslJoinBuilder<>(getQueryMixin(), ref, target); + } + + /** + * Define a join. + * + * @param ref reference + * @param target join target + * @return new instance of {@link QuerydslJoinBuilder}. + */ + public QuerydslJoinBuilder join(CollectionPathBase ref, Path target) { + return new QuerydslJoinBuilder<>(getQueryMixin(), ref, target); + } + + /** + * Define a constraint for an embedded object. + * + * @param collection collection must not be {@literal null}. + * @param target target must not be {@literal null}. + * @return new instance of {@link QuerydslAnyEmbeddedBuilder}. + */ + public QuerydslAnyEmbeddedBuilder anyEmbedded(Path> collection, Path target) { + return new QuerydslAnyEmbeddedBuilder<>(getQueryMixin(), collection); + } + + protected org.springframework.data.mongodb.core.query.Query createQuery() { + + QueryMetadata metadata = getQueryMixin().getMetadata(); + + return createQuery(createFilter(metadata), metadata.getProjection(), metadata.getModifiers(), + metadata.getOrderBy()); + } + + protected org.springframework.data.mongodb.core.query.Query createQuery(@Nullable Predicate filter, + @Nullable Expression projection, QueryModifiers modifiers, List> orderBy) { + + BasicQuery basicQuery = new BasicQuery(createQuery(filter), createProjection(projection)); + + 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.setSortObject(createSort(orderBy)); + } + + return basicQuery; + } + + @Nullable + protected Predicate createFilter(QueryMetadata metadata) { + + Predicate filter; + if (!metadata.getJoins().isEmpty()) { + filter = ExpressionUtils.allOf(metadata.getWhere(), createJoinFilter(metadata)); + } else { + filter = metadata.getWhere(); + } + return filter; + } + + @SuppressWarnings("unchecked") + @Nullable + protected Predicate createJoinFilter(QueryMetadata metadata) { + + LinkedMultiValueMap, Predicate> predicates = new LinkedMultiValueMap<>(); + List joins = metadata.getJoins(); + + for (int i = joins.size() - 1; i >= 0; i--) { + + JoinExpression join = joins.get(i); + Path source = (Path) ((Operation) join.getTarget()).getArg(0); + Path target = (Path) ((Operation) join.getTarget()).getArg(1); + Collection extraFilters = predicates.get(target.getRoot()); + Predicate filter = ExpressionUtils.allOf(join.getCondition(), allOf(extraFilters)); + + List ids = getIds(target.getType(), filter); + + if (ids.isEmpty()) { + return ExpressionUtils.predicate(QuerydslMongoOps.NO_MATCH, source); + } + + Path path = ExpressionUtils.path(String.class, source, "$id"); + predicates.add(source.getRoot(), ExpressionUtils.in((Path) path, ids)); + } + + Path source = (Path) ((Operation) joins.get(0).getTarget()).getArg(0); + return allOf(predicates.get(source.getRoot())); + } + + private Predicate allOf(Collection predicates) { + return predicates != null ? ExpressionUtils.allOf(predicates) : null; + } + + /** + * Fetch the list of ids matching a given condition. + * + * @param targetType must not be {@literal null}. + * @param condition must not be {@literal null}. + * @return empty {@link List} if none found. + */ + protected List getIds(Class targetType, Predicate condition) { + + Query query = createQuery(condition, null, QueryModifiers.EMPTY, Collections.emptyList()); + return mongoOperations.findDistinct(query, "_id", targetType, Object.class); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslJoinBuilder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslJoinBuilder.java new file mode 100644 index 0000000000..ac4b4bf6e4 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslJoinBuilder.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.support; + +import com.querydsl.core.JoinType; +import com.querydsl.core.support.QueryMixin; +import com.querydsl.core.types.ExpressionUtils; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.Predicate; + +/** + * {@code QuerydslJoinBuilder} is a builder for join constraints. + *

+ * Original implementation source {@link com.querydsl.mongodb.JoinBuilder} by {@literal The Querydsl Team} + * (http://www.querydsl.com/team) licensed under the Apache License, Version + * 2.0. + *

+ * Modified for usage with {@link QuerydslAbstractMongodbQuery}. + * + * @param + * @param + * @author tiwe + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.1 + */ +public class QuerydslJoinBuilder, K, T> { + + private final QueryMixin queryMixin; + private final Path ref; + private final Path target; + + QuerydslJoinBuilder(QueryMixin queryMixin, Path ref, Path target) { + + this.queryMixin = queryMixin; + this.ref = ref; + this.target = target; + } + + /** + * Add the given join conditions. + * + * @param conditions must not be {@literal null}. + * @return the target {@link QueryMixin}. + * @see QueryMixin#on(Predicate) + */ + @SuppressWarnings("unchecked") + public Q on(Predicate... conditions) { + + queryMixin.addJoin(JoinType.JOIN, ExpressionUtils.as((Path) ref, target)); + queryMixin.on(conditions); + return queryMixin.getSelf(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoOps.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoOps.java new file mode 100644 index 0000000000..cae326188a --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoOps.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.support; + +import com.querydsl.core.types.Operator; + +/** + * Spring Data specific {@link Operator operators} for usage with Querydsl and MongoDB. + * + * @author Christoph Strobl + * @since 2.1 + */ +enum QuerydslMongoOps implements Operator { + + /** + * {@link Operator} always evaluating to {@literal false}. + */ + NO_MATCH(Boolean.class); + + private final Class type; + + QuerydslMongoOps(Class type) { + this.type = type; + } + + @Override + public Class getType() { + return type; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java index 19bf1782b2..0ec3328509 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java @@ -39,7 +39,6 @@ import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; import com.querydsl.core.types.dsl.PathBuilder; -import com.querydsl.mongodb.AbstractMongodbQuery; /** * MongoDB-specific {@link QuerydslPredicateExecutor} that allows execution {@link Predicate}s in various forms. @@ -166,7 +165,7 @@ public Page findAll(Predicate predicate, Pageable pageable) { Assert.notNull(predicate, "Predicate must not be null!"); Assert.notNull(pageable, "Pageable must not be null!"); - AbstractMongodbQuery> query = createQueryFor(predicate); + SpringDataMongodbQuery query = createQueryFor(predicate); return PageableExecutionUtils.getPage(applyPagination(query, pageable).fetch(), pageable, query::fetchCount); } @@ -196,47 +195,45 @@ public boolean exists(Predicate predicate) { } /** - * Creates a {@link AbstractMongodbQuery} for the given {@link Predicate}. + * Creates a {@link SpringDataMongodbQuery} for the given {@link Predicate}. * * @param predicate * @return */ - private AbstractMongodbQuery> createQueryFor(Predicate predicate) { + private SpringDataMongodbQuery createQueryFor(Predicate predicate) { return createQuery().where(predicate); } /** - * Creates a {@link AbstractMongodbQuery}. + * Creates a {@link SpringDataMongodbQuery}. * * @return */ - private AbstractMongodbQuery> createQuery() { + private SpringDataMongodbQuery createQuery() { return new SpringDataMongodbQuery<>(mongoOperations, entityInformation.getJavaType()); } /** - * Applies the given {@link Pageable} to the given {@link MongodbQuery}. + * Applies the given {@link Pageable} to the given {@link SpringDataMongodbQuery}. * * @param query * @param pageable * @return */ - private AbstractMongodbQuery> applyPagination( - AbstractMongodbQuery> query, Pageable pageable) { + private SpringDataMongodbQuery applyPagination(SpringDataMongodbQuery query, Pageable pageable) { query = query.offset(pageable.getOffset()).limit(pageable.getPageSize()); return applySorting(query, pageable.getSort()); } /** - * Applies the given {@link Sort} to the given {@link MongodbQuery}. + * Applies the given {@link Sort} to the given {@link SpringDataMongodbQuery}. * * @param query * @param sort * @return */ - private AbstractMongodbQuery> applySorting( - AbstractMongodbQuery> query, Sort sort) { + private SpringDataMongodbQuery applySorting(SpringDataMongodbQuery query, Sort sort) { // TODO: find better solution than instanceof check if (sort instanceof QSort) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupport.java index da39c9e73d..5b89e85a1e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupport.java @@ -21,7 +21,6 @@ import org.springframework.util.Assert; import com.querydsl.core.types.EntityPath; -import com.querydsl.mongodb.AbstractMongodbQuery; /** * Base class to create repository implementations based on Querydsl. @@ -48,13 +47,13 @@ public QuerydslRepositorySupport(MongoOperations operations) { } /** - * Returns a {@link MongodbQuery} for the given {@link EntityPath}. The collection being queried is derived from the + * Returns a {@link SpringDataMongodbQuery} for the given {@link EntityPath}. The collection being queried is derived from the * entity metadata. * * @param path * @return */ - protected AbstractMongodbQuery> from(final EntityPath path) { + protected SpringDataMongodbQuery from(final EntityPath path) { Assert.notNull(path, "EntityPath must not be null!"); MongoPersistentEntity entity = context.getRequiredPersistentEntity(path.getType()); @@ -62,13 +61,13 @@ protected AbstractMongodbQuery> from(final Enti } /** - * Returns a {@link MongodbQuery} for the given {@link EntityPath} querying the given collection. + * Returns a {@link SpringDataMongodbQuery} for the given {@link EntityPath} querying the given collection. * * @param path must not be {@literal null} * @param collection must not be blank or {@literal null} * @return */ - protected AbstractMongodbQuery> from(final EntityPath path, String collection) { + protected SpringDataMongodbQuery from(final EntityPath path, String collection) { Assert.notNull(path, "EntityPath must not be null!"); Assert.hasText(collection, "Collection name must not be null or empty!"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java index fa7185468d..a5662147ad 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java @@ -15,26 +15,17 @@ */ package org.springframework.data.mongodb.repository.support; -import org.bson.Document; import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.lang.Nullable; - -import com.google.common.base.Function; -import com.mongodb.BasicDBObject; -import com.mongodb.DBCollection; -import com.mongodb.DBObject; -import com.querydsl.mongodb.AbstractMongodbQuery; /** - * Spring Data specific {@link AbstractMongodbQuery} implementation. + * Spring Data specific simple {@link com.querydsl.core.Fetchable} {@link com.querydsl.core.SimpleQuery Query} + * implementation. * * @author Oliver Gierke * @author Mark Paluch + * @author Christoph Strobl */ -public class SpringDataMongodbQuery extends AbstractMongodbQuery> { - - private final MongoOperations operations; +public class SpringDataMongodbQuery extends QuerydslFetchableMongodbQuery> { /** * Creates a new {@link SpringDataMongodbQuery}. @@ -56,25 +47,6 @@ public SpringDataMongodbQuery(final MongoOperations operations, final Class type, String collectionName) { - super(((MongoTemplate) operations).getMongoDbFactory().getLegacyDb().getCollection(collectionName), - new Function() { - - @Override - public T apply(@Nullable DBObject input) { - return operations.getConverter().read(type, new Document((BasicDBObject) input)); - } - }, new SpringDataMongodbSerializer(operations.getConverter())); - - this.operations = operations; - } - - /* - * (non-Javadoc) - * @see com.querydsl.mongodb.AbstractMongodbQuery#getCollection(java.lang.Class) - */ - @Override - protected DBCollection getCollection(Class type) { - return ((MongoTemplate) operations).getMongoDbFactory().getLegacyDb() - .getCollection(operations.getCollectionName(type)); + super(new SpringDataMongodbSerializer(operations.getConverter()), type, collectionName, operations); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java index 7add845d41..8a2e8f4f4f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java @@ -17,7 +17,6 @@ import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Optional; import java.util.Set; import java.util.regex.Pattern; @@ -28,16 +27,11 @@ import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import com.mongodb.BasicDBList; -import com.mongodb.BasicDBObject; -import com.mongodb.DBObject; import com.mongodb.DBRef; -import com.mongodb.util.JSON; import com.querydsl.core.types.Constant; import com.querydsl.core.types.Expression; import com.querydsl.core.types.Operation; @@ -53,14 +47,14 @@ * @author Christoph Strobl * @author Mark Paluch */ -class SpringDataMongodbSerializer extends MongodbSerializer { +class SpringDataMongodbSerializer extends MongodbDocumentSerializer { private static final String ID_KEY = "_id"; private static final Set PATH_TYPES; static { - Set pathTypes = new HashSet(); + Set pathTypes = new HashSet<>(); pathTypes.add(PathType.VARIABLE); pathTypes.add(PathType.PROPERTY); @@ -72,9 +66,9 @@ class SpringDataMongodbSerializer extends MongodbSerializer { private final QueryMapper mapper; /** - * Creates a new {@link SpringDataMongodbSerializer} for the given {@link MappingContext}. + * Creates a new {@link SpringDataMongodbSerializer} for the given {@link MongoConverter}. * - * @param mappingContext must not be {@literal null}. + * @param converter must not be {@literal null}. */ public SpringDataMongodbSerializer(MongoConverter converter) { @@ -96,7 +90,7 @@ public Object visit(Constant expr, Void context) { return super.visit(expr, context); } - return toQuerydslMongoType(expr.getConstant()); + return converter.convertToMongoType(expr.getConstant()); } /* @@ -119,10 +113,10 @@ protected String getKeyForPath(Path expr, PathMetadata metadata) { /* * (non-Javadoc) - * @see com.querydsl.mongodb.MongodbSerializer#asDBObject(java.lang.String, java.lang.Object) + * @see org.springframework.data.mongodb.repository.support.MongodbSerializer#asDocument(java.lang.String, java.lang.Object) */ @Override - protected DBObject asDBObject(@Nullable String key, @Nullable Object value) { + protected Document asDocument(@Nullable String key, @Nullable Object value) { value = value instanceof Optional ? ((Optional) value).orElse(null) : value; @@ -130,7 +124,7 @@ protected DBObject asDBObject(@Nullable String key, @Nullable Object value) { return convertId(key, value); } - return super.asDBObject(key, value instanceof Pattern ? value : toQuerydslMongoType(value)); + return super.asDocument(key, value instanceof Pattern ? value : converter.convertToMongoType(value)); } /** @@ -141,13 +135,11 @@ protected DBObject asDBObject(@Nullable String key, @Nullable Object value) { * @param idValue the raw {@literal id} value. * @return the {@literal id} representation in the required format. */ - private DBObject convertId(String key, Object idValue) { + private Document convertId(String key, Object idValue) { Object convertedId = mapper.convertId(idValue); - Document mappedIdValue = mapper.getMappedObject((BasicDBObject) super.asDBObject(key, convertedId), - Optional.empty()); - return (DBObject) JSON.parse(mappedIdValue.toJson()); + return mapper.getMappedObject(super.asDocument(key, convertedId), Optional.empty()); } /* @@ -215,6 +207,7 @@ protected Object convert(@Nullable Path path, @Nullable Constant constant) : asReference(constant.getConstant(), path); } + @Nullable private MongoPersistentProperty getPropertyFor(Path path) { Path parent = path.getMetadata().getParent(); @@ -224,7 +217,7 @@ private MongoPersistentProperty getPropertyFor(Path path) { } MongoPersistentEntity entity = mappingContext.getPersistentEntity(parent.getType()); - return entity != null ? entity.getRequiredPersistentProperty(path.getMetadata().getName()) : null; + return entity != null ? entity.getPersistentProperty(path.getMetadata().getName()) : null; } /** @@ -250,25 +243,4 @@ private MongoPersistentProperty getPropertyForPotentialDbRef(Path path) { return property; } - - private Object toQuerydslMongoType(Object source) { - - Object target = converter.convertToMongoType(source); - - if (target instanceof List) { - - List newList = new BasicDBList(); - - for (Object item : (List) target) { - if (item instanceof Document) { - newList.add(new BasicDBObject(BsonUtils.asMap((Document) item))); - } else { - newList.add(item); - } - } - return newList; - } - - return target; - } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java index dcca0927a0..dbd64e76ff 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java @@ -40,6 +40,11 @@ import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.PersonPojoStringId; +import org.springframework.data.mongodb.repository.Person; +import org.springframework.data.mongodb.repository.QPerson; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.mongodb.repository.support.MongoRepositoryFactory; +import org.springframework.data.mongodb.repository.support.QuerydslMongoPredicateExecutor; import com.mongodb.MongoClient; import com.mongodb.WriteConcern; @@ -386,6 +391,29 @@ public void publishesAfterConvertEventForFindQueriesUsingProjections() { assertThat(listener.onAfterConvertEvents.get(0).getCollectionName()).isEqualTo(COLLECTION_NAME); } + @Test // DATAMONGO-700, DATAMONGO-1185, DATAMONGO-1848 + public void publishesEventsForQuerydslFindQueries() { + + template.dropCollection(Person.class); + + template.save(new Person("Boba", "Fett", 40)); + + MongoRepositoryFactory factory = new MongoRepositoryFactory(template); + MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + QuerydslMongoPredicateExecutor executor = new QuerydslMongoPredicateExecutor<>(entityInformation, template); + + executor.findOne(QPerson.person.lastname.startsWith("Fe")); + + assertThat(listener.onAfterLoadEvents).hasSize(1); + assertThat(listener.onAfterLoadEvents.get(0).getCollectionName()).isEqualTo("person"); + + assertThat(listener.onBeforeConvertEvents).hasSize(1); + assertThat(listener.onBeforeConvertEvents.get(0).getCollectionName()).isEqualTo("person"); + + assertThat(listener.onAfterConvertEvents).hasSize(1); + assertThat(listener.onAfterConvertEvents.get(0).getCollectionName()).isEqualTo("person"); + } + private void comparePersonAndDocument(PersonPojoStringId p, PersonPojoStringId p2, org.bson.Document document) { assertThat(p2.getId()).isEqualTo(p.getId()); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java index 6dbede532e..36dacb0ed2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.*; import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.List; import org.junit.Before; @@ -25,15 +26,25 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.dao.PermissionDeniedDataAccessException; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.repository.Address; import org.springframework.data.mongodb.repository.Person; +import org.springframework.data.mongodb.repository.QAddress; import org.springframework.data.mongodb.repository.QPerson; +import org.springframework.data.mongodb.repository.QUser; +import org.springframework.data.mongodb.repository.User; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import com.mongodb.MongoException; +import com.mongodb.client.MongoDatabase; + /** * Integration test for {@link QuerydslMongoPredicateExecutor}. * @@ -47,6 +58,8 @@ public class QuerydslMongoPredicateExecutorIntegrationTests { @Autowired MongoOperations operations; + @Autowired MongoDbFactory dbFactory; + QuerydslMongoPredicateExecutor repository; Person dave, oliver, carter; @@ -57,7 +70,7 @@ public void setup() { MongoRepositoryFactory factory = new MongoRepositoryFactory(operations); MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); - repository = new QuerydslMongoPredicateExecutor(entityInformation, operations); + repository = new QuerydslMongoPredicateExecutor<>(entityInformation, operations); operations.dropCollection(Person.class); @@ -99,4 +112,114 @@ public void findOneWithPredicateReturnsOptionalEmptyWhenNoDataFound() { public void findOneWithPredicateThrowsExceptionForNonUniqueResults() { repository.findOne(person.firstname.contains("e")); } + + @Test // DATAMONGO-1848 + public void findUsingAndShouldWork() { + + assertThat(repository.findAll( + person.lastname.startsWith(oliver.getLastname()).and(person.firstname.startsWith(dave.getFirstname())))) + .containsExactly(dave); + } + + @Test // DATAMONGO-362, DATAMONGO-1848 + public void springDataMongodbQueryShouldAllowJoinOnDBref() { + + User user1 = new User(); + user1.setUsername("user-1"); + + User user2 = new User(); + user2.setUsername("user-2"); + + User user3 = new User(); + user3.setUsername("user-3"); + + operations.save(user1); + operations.save(user2); + operations.save(user3); + + Person person1 = new Person("Max", "The Mighty"); + person1.setCoworker(user1); + + Person person2 = new Person("Jack", "The Ripper"); + person2.setCoworker(user2); + + Person person3 = new Person("Bob", "The Builder"); + person3.setCoworker(user3); + + operations.save(person1); + operations.save(person2); + operations.save(person3); + + List result = new SpringDataMongodbQuery<>(operations, Person.class).where() + .join(person.coworker, QUser.user).on(QUser.user.username.eq("user-2")).fetch(); + + assertThat(result).containsExactly(person2); + } + + @Test // DATAMONGO-362, DATAMONGO-1848 + public void springDataMongodbQueryShouldReturnEmptyOnJoinWithNoResults() { + + User user1 = new User(); + user1.setUsername("user-1"); + + User user2 = new User(); + user2.setUsername("user-2"); + + operations.save(user1); + operations.save(user2); + + Person person1 = new Person("Max", "The Mighty"); + person1.setCoworker(user1); + + Person person2 = new Person("Jack", "The Ripper"); + person2.setCoworker(user2); + + operations.save(person1); + operations.save(person2); + + List result = new SpringDataMongodbQuery<>(operations, Person.class).where() + .join(person.coworker, QUser.user).on(QUser.user.username.eq("does-not-exist")).fetch(); + + assertThat(result).isEmpty(); + } + + @Test // DATAMONGO-595, DATAMONGO-1848 + public void springDataMongodbQueryShouldAllowElemMatchOnArrays() { + + Address adr1 = new Address("Hauptplatz", "4020", "Linz"); + Address adr2 = new Address("Stephansplatz", "1010", "Wien"); + Address adr3 = new Address("Tower of London", "EC3N 4AB", "London"); + + Person person1 = new Person("Max", "The Mighty"); + person1.setShippingAddresses(new LinkedHashSet<>(Arrays.asList(adr1, adr2))); + + Person person2 = new Person("Jack", "The Ripper"); + person2.setShippingAddresses(new LinkedHashSet<>(Arrays.asList(adr2, adr3))); + + operations.save(person1); + operations.save(person2); + + List result = new SpringDataMongodbQuery<>(operations, Person.class).where() + .anyEmbedded(person.shippingAddresses, QAddress.address).on(QAddress.address.city.eq("London")).fetch(); + + assertThat(result).containsExactly(person2); + } + + @Test(expected = PermissionDeniedDataAccessException.class) // DATAMONGO-1434, DATAMONGO-1848 + public void translatesExceptionsCorrectly() { + + MongoOperations ops = new MongoTemplate(dbFactory) { + + @Override + protected MongoDatabase doGetDatabase() { + throw new MongoException(18, "Authentication Failed"); + } + }; + + MongoRepositoryFactory factory = new MongoRepositoryFactory(ops); + MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + repository = new QuerydslMongoPredicateExecutor<>(entityInformation, ops); + + repository.findOne(person.firstname.contains("batman")); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupportTests.java index acaa188454..e8fd4b7c4f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupportTests.java @@ -26,6 +26,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.MongoOperations; @@ -139,6 +140,79 @@ public void shouldConvertStringIdThatIsAValidObjectIdIntoTheSuch() { assertThat(query.fetchOne(), equalTo(outer)); } + @Test // DATAMONGO-1810, DATAMONGO-1848 + public void shouldFetchObjectsViaStringWhenUsingInOnDbRef() { + + User bart = new User(); + DirectFieldAccessor dfa = new DirectFieldAccessor(bart); + dfa.setPropertyValue("id", "bart"); + + bart.setUsername("bart@simpson.com"); + operations.save(bart); + + User lisa = new User(); + dfa = new DirectFieldAccessor(lisa); + dfa.setPropertyValue("id", "lisa"); + + lisa.setUsername("lisa@simposon.com"); + operations.save(lisa); + + person.setCoworker(bart); + operations.save(person); + + QPerson p = QPerson.person; + + SpringDataMongodbQuery queryUsingIdFieldWithinInClause = repoSupport.from(p) + .where(p.coworker.id.in(Arrays.asList(bart.getId(), lisa.getId()))); + + SpringDataMongodbQuery queryUsingRefObject = repoSupport.from(p).where(p.coworker.eq(bart)); + + assertThat(queryUsingIdFieldWithinInClause.fetchOne(), equalTo(person)); + assertThat(queryUsingIdFieldWithinInClause.fetchOne(), equalTo(queryUsingRefObject.fetchOne())); + } + + @Test // DATAMONGO-1810, DATAMONGO-1848 + public void shouldFetchObjectsViaStringStoredAsObjectIdWhenUsingInOnDbRef() { + + User bart = new User(); + bart.setUsername("bart@simpson.com"); + operations.save(bart); + + User lisa = new User(); + lisa.setUsername("lisa@simposon.com"); + operations.save(lisa); + + person.setCoworker(bart); + operations.save(person); + + QPerson p = QPerson.person; + + SpringDataMongodbQuery queryUsingIdFieldWithinInClause = repoSupport.from(p) + .where(p.coworker.id.in(Arrays.asList(bart.getId(), lisa.getId()))); + + SpringDataMongodbQuery queryUsingRefObject = repoSupport.from(p).where(p.coworker.eq(bart)); + + assertThat(queryUsingIdFieldWithinInClause.fetchOne(), equalTo(person)); + assertThat(queryUsingIdFieldWithinInClause.fetchOne(), equalTo(queryUsingRefObject.fetchOne())); + } + + @Test // DATAMONGO-1848, DATAMONGO-2010 + public void shouldConvertStringIdThatIsAValidObjectIdWhenUsedInInPredicateIntoTheSuch() { + + Outer outer = new Outer(); + outer.id = new ObjectId().toHexString(); + outer.inner = new Inner(); + outer.inner.id = new ObjectId().toHexString(); + outer.inner.value = "eat sleep workout repeat"; + + operations.save(outer); + + QQuerydslRepositorySupportTests_Outer o = QQuerydslRepositorySupportTests_Outer.outer; + SpringDataMongodbQuery query = repoSupport.from(o).where(o.inner.id.in(outer.inner.id, outer.inner.id)); + + assertThat(query.fetchOne(), equalTo(outer)); + } + @Data @Document public static class Outer { @@ -152,6 +226,5 @@ public static class Inner { @Id String id; String value; - } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializerUnitTests.java index 55faae109d..14f51c091e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializerUnitTests.java @@ -19,6 +19,7 @@ import static org.junit.Assert.*; import static org.springframework.data.mongodb.core.DocumentTestUtils.*; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -42,10 +43,6 @@ import org.springframework.data.mongodb.repository.QAddress; import org.springframework.data.mongodb.repository.QPerson; -import com.mongodb.BasicDBList; -import com.mongodb.BasicDBObject; -import com.mongodb.DBObject; -import com.mongodb.util.JSON; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.BooleanOperation; import com.querydsl.core.types.dsl.PathBuilder; @@ -96,9 +93,7 @@ public void convertsComplexObjectOnSerializing() { address.street = "Foo"; address.zipCode = "01234"; - DBObject result = serializer.asDBObject("foo", address); - assertThat(result, is(instanceOf(BasicDBObject.class))); - BasicDBObject document = (BasicDBObject) result; + Document document = serializer.asDocument("foo", address); Object value = document.get("foo"); assertThat(value, is(notNullValue())); @@ -123,10 +118,10 @@ public void convertsIdPropertyCorrectly() { PathBuilder
builder = new PathBuilder
(Address.class, "address"); StringPath idPath = builder.getString("id"); - DBObject result = (DBObject) serializer.visit((BooleanOperation) idPath.eq(id.toString()), (Void) null); + Document result = (Document) serializer.visit((BooleanOperation) idPath.eq(id.toString()), null); assertThat(result.get("_id"), is(notNullValue())); assertThat(result.get("_id"), is(instanceOf(ObjectId.class))); - assertThat(result.get("_id"), is((Object) id)); + assertThat(result.get("_id"), is(id)); } @Test // DATAMONGO-761 @@ -143,10 +138,10 @@ public void looksUpKeyForNonPropertyPath() { public void shouldConvertObjectIdEvenWhenNestedInOperatorDbObject() { ObjectId value = new ObjectId("53bb9fd14438765b29c2d56e"); - DBObject serialized = serializer.asDBObject("_id", new Document("$ne", value.toString())); + Document serialized = serializer.asDocument("_id", new Document("$ne", value.toString())); - DBObject _id = getTypedValue(new Document(serialized.toMap()), "_id", DBObject.class); - ObjectId $ne = getTypedValue(new Document(_id.toMap()), "$ne", ObjectId.class); + Document _id = getTypedValue(serialized, "_id", Document.class); + ObjectId $ne = getTypedValue(_id, "$ne", ObjectId.class); assertThat($ne, is(value)); } @@ -156,14 +151,14 @@ public void shouldConvertCollectionOfObjectIdEvenWhenNestedInOperatorDocument() ObjectId firstId = new ObjectId("53bb9fd14438765b29c2d56e"); ObjectId secondId = new ObjectId("53bb9fda4438765b29c2d56f"); - BasicDBList objectIds = new BasicDBList(); + List objectIds = new ArrayList<>(); objectIds.add(firstId.toString()); objectIds.add(secondId.toString()); - DBObject serialized = serializer.asDBObject("_id", new Document("$in", objectIds)); + Document serialized = serializer.asDocument("_id", new Document("$in", objectIds)); - DBObject _id = getTypedValue(new Document(serialized.toMap()), "_id", DBObject.class); - List $in = getTypedValue(new Document(_id.toMap()), "$in", List.class); + Document _id = getTypedValue(serialized, "_id", Document.class); + List $in = getTypedValue(_id, "$in", List.class); assertThat($in, IsIterableContainingInOrder. contains(firstId, secondId)); } @@ -182,18 +177,19 @@ public void takesCustomConversionForEnumsIntoAccount() { Object mappedPredicate = this.serializer.handle(QPerson.person.sex.eq(Sex.FEMALE)); - assertThat(mappedPredicate, is(instanceOf(DBObject.class))); - assertThat(((DBObject) mappedPredicate).get("sex"), is((Object) "f")); + assertThat(mappedPredicate, is(instanceOf(Document.class))); + assertThat(((Document) mappedPredicate).get("sex"), is("f")); } - @Test // DATAMONGO-1943 + @Test // DATAMONGO-1848, DATAMONGO-1943 public void shouldRemarshallListsAndDocuments() { - BooleanExpression criteria = QPerson.person.firstname.isNotEmpty() + BooleanExpression criteria = QPerson.person.lastname.isNotEmpty() .and(QPerson.person.firstname.containsIgnoreCase("foo")).not(); - assertThat(this.serializer.handle(criteria), is(equalTo(JSON.parse("{ \"$or\" : [ { \"firstname\" : { \"$ne\" : { " - + "\"$ne\" : \"\"}}} , { \"firstname\" : { \"$not\" : { \"$regex\" : \".*\\\\Qfoo\\\\E.*\" , \"$options\" : \"i\"}}}]}")))); + assertThat(this.serializer.handle(criteria), + is(equalTo(Document.parse("{ \"$or\" : [ { \"lastname\" : { \"$not\" : { " + + "\"$ne\" : \"\"}}} , { \"firstname\" : { \"$not\" : { \"$regex\" : \".*\\\\Qfoo\\\\E.*\" , \"$options\" : \"i\"}}}]}")))); } class Address {