From b7755e71f6fc5347e1c0652bda714f64a74df339 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 18 Jun 2018 14:34:44 +0200 Subject: [PATCH] DATAMONGO-1848 - Import Document-based Querydsl support. Original Pull Request: #579 --- .../support/AbstractMongodbQuery.java | 181 +++++++++ .../support/AnyEmbeddedBuilder.java | 47 +++ .../support/FetchableMongodbQuery.java | 260 +++++++++++++ .../repository/support/JoinBuilder.java | 51 +++ .../support/MongodbDocumentSerializer.java | 361 ++++++++++++++++++ .../support/MongodbExpressions.java | 61 +++ 6 files changed, 961 insertions(+) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AbstractMongodbQuery.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AnyEmbeddedBuilder.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableMongodbQuery.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/JoinBuilder.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbExpressions.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AbstractMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AbstractMongodbQuery.java new file mode 100644 index 000000000..861e43d73 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AbstractMongodbQuery.java @@ -0,0 +1,181 @@ +/* + * 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 javax.annotation.Nullable; + +import org.bson.Document; + +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.Path; +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.CollectionPathBase; + +/** + * {@code AbstractMongodbQuery} provides a base class for general Querydsl query implementation. + * + * @author Mark Paluch + * @param concrete subtype + */ +abstract class AbstractMongodbQuery> implements SimpleQuery { + + @SuppressWarnings("serial") + static class NoResults extends RuntimeException {} + + private final MongodbDocumentSerializer serializer; + private final QueryMixin queryMixin; + + /** + * Create a new MongodbQuery instance + * + * @param serializer serializer + */ + @SuppressWarnings("unchecked") + public AbstractMongodbQuery(MongodbDocumentSerializer serializer) { + @SuppressWarnings("unchecked") // Q is this plus subclass + Q query = (Q) this; + this.queryMixin = new QueryMixin(query, new DefaultQueryMetadata(), false); + this.serializer = serializer; + } + + /** + * Define a join + * + * @param ref reference + * @param target join target + * @return join builder + */ + public JoinBuilder join(Path ref, Path target) { + return new JoinBuilder(queryMixin, ref, target); + } + + /** + * Define a join + * + * @param ref reference + * @param target join target + * @return join builder + */ + public JoinBuilder join(CollectionPathBase ref, Path target) { + return new JoinBuilder(queryMixin, ref, target); + } + + /** + * Define a constraint for an embedded object + * + * @param collection collection + * @param target target + * @return builder + */ + public AnyEmbeddedBuilder anyEmbedded(Path> collection, Path target) { + return new AnyEmbeddedBuilder(queryMixin, collection); + } + + @Override + public Q distinct() { + return queryMixin.distinct(); + } + + public Q where(Predicate e) { + return queryMixin.where(e); + } + + @Override + public Q where(Predicate... e) { + return queryMixin.where(e); + } + + @Override + public Q limit(long limit) { + return queryMixin.limit(limit); + } + + @Override + public Q offset(long offset) { + return queryMixin.offset(offset); + } + + @Override + public Q restrict(QueryModifiers modifiers) { + return queryMixin.restrict(modifiers); + } + + public Q orderBy(OrderSpecifier o) { + return queryMixin.orderBy(o); + } + + @Override + public Q orderBy(OrderSpecifier... o) { + return queryMixin.orderBy(o); + } + + @Override + public Q set(ParamExpression param, T value) { + return queryMixin.set(param, value); + } + + protected Document createProjection(Expression projection) { + if (projection instanceof FactoryExpression) { + Document obj = new Document(); + for (Object expr : ((FactoryExpression) projection).getArgs()) { + if (expr instanceof Expression) { + obj.put((String) serializer.handle((Expression) expr), 1); + } + } + return obj; + } + return null; + } + + protected Document createQuery(@Nullable Predicate predicate) { + if (predicate != null) { + return (Document) serializer.handle(predicate); + } else { + return new Document(); + } + } + + QueryMixin getQueryMixin() { + return queryMixin; + } + + MongodbDocumentSerializer getSerializer() { + return serializer; + } + + /** + * Get the where definition as a Document instance + * + * @return + */ + public 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/AnyEmbeddedBuilder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AnyEmbeddedBuilder.java new file mode 100644 index 000000000..e2260fd27 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AnyEmbeddedBuilder.java @@ -0,0 +1,47 @@ +/* + * 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 AnyEmbeddedBuilder} is a builder for constraints on embedded objects + * + * @param query type + * @author Mark Paluch + */ +class AnyEmbeddedBuilder> { + + private final QueryMixin queryMixin; + + private final Path> collection; + + public AnyEmbeddedBuilder(QueryMixin queryMixin, Path> collection) { + this.queryMixin = queryMixin; + this.collection = collection; + } + + 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/FetchableMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableMongodbQuery.java new file mode 100644 index 000000000..41f053721 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableMongodbQuery.java @@ -0,0 +1,260 @@ +/* + * 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 javax.annotation.Nullable; + +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.Query; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +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; + +/** + * {@link Fetchable} Mongodb query with a pluggable Document to Bean transformation. + * + * @param result type + * @param concrete subtype + * @author Mark Paluch + */ +class FetchableMongodbQuery extends AbstractMongodbQuery> implements Fetchable { + + private final Class entityClass; + private final String collection; + private final MongoOperations mongoOperations; + + public FetchableMongodbQuery(MongodbDocumentSerializer serializer, Class entityClass, + MongoOperations mongoOperations) { + + super(serializer); + + this.entityClass = (Class) entityClass; + this.collection = mongoOperations.getCollectionName(entityClass); + this.mongoOperations = mongoOperations; + } + + public FetchableMongodbQuery(MongodbDocumentSerializer serializer, Class entityClass, String collection, + MongoOperations mongoOperations) { + + super(serializer); + + this.entityClass = (Class) entityClass; + this.collection = collection; + this.mongoOperations = mongoOperations; + } + + /** + * Iterate with the specific fields + * + * @param paths fields to return + * @return iterator + */ + public CloseableIterator iterate(Path... paths) { + getQueryMixin().setProjection(paths); + return iterate(); + } + + @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() { + + } + + @Override + public void close() { + stream.close(); + } + }; + } + + /** + * Fetch with the specific fields + * + * @param paths fields to return + * @return results + */ + public List fetch(Path... paths) { + getQueryMixin().setProjection(paths); + return fetch(); + } + + @Override + public List fetch() { + return mongoOperations.query(entityClass).matching(createQuery()).all(); + } + + /** + * Fetch first with the specific fields + * + * @param paths fields to return + * @return first result + */ + public K fetchFirst(Path... paths) { + getQueryMixin().setProjection(paths); + return fetchFirst(); + } + + @Override + public K fetchFirst() { + return mongoOperations.query(entityClass).matching(createQuery()).firstValue(); + } + + /** + * Fetch one with the specific fields + * + * @param paths fields to return + * @return first result + */ + public K fetchOne(Path... paths) { + getQueryMixin().setProjection(paths); + return fetchOne(); + } + + @Override + public K fetchOne() { + return mongoOperations.query(entityClass).matching(createQuery()).oneValue(); + + } + + /** + * Fetch results with the specific fields + * + * @param paths fields to return + * @return results + */ + public QueryResults fetchResults(Path... paths) { + getQueryMixin().setProjection(paths); + return fetchResults(); + } + + @Override + public QueryResults fetchResults() { + long total = fetchCount(); + if (total > 0L) { + return new QueryResults<>(fetch(), getQueryMixin().getMetadata().getModifiers(), total); + } else { + return QueryResults.emptyResults(); + } + } + + @Override + public long fetchCount() { + return mongoOperations.query(entityClass).matching(createQuery()).count(); + } + + protected org.springframework.data.mongodb.core.query.Query createQuery() { + QueryMetadata metadata = getQueryMixin().getMetadata(); + Predicate filter = createFilter(metadata); + return createQuery(filter, metadata.getProjection(), metadata.getModifiers(), metadata.getOrderBy()); + } + + protected org.springframework.data.mongodb.core.query.Query createQuery(@Nullable Predicate where, + Expression projection, QueryModifiers modifiers, List> orderBy) { + + BasicQuery basicQuery = new BasicQuery(createQuery(where), 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(getSerializer().toSort(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) { + Multimap, Predicate> predicates = HashMultimap.create(); + 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()) { + throw new NoResults(); + } + Path path = ExpressionUtils.path(String.class, source, "$id"); + predicates.put(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; + } + + protected List getIds(Class targetType, Predicate condition) { + // TODO : fetch only ids + 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/JoinBuilder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/JoinBuilder.java new file mode 100644 index 000000000..4459ab1ed --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/JoinBuilder.java @@ -0,0 +1,51 @@ +/* + * 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 JoinBuilder} is a builder for join constraints + * + * @author Mark Paluch + * @param + * @param + */ +class JoinBuilder, T> { + + private final QueryMixin queryMixin; + + private final Path ref; + + private final Path target; + + public JoinBuilder(QueryMixin queryMixin, Path ref, Path target) { + this.queryMixin = queryMixin; + this.ref = ref; + this.target = target; + } + + @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/MongodbDocumentSerializer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java new file mode 100644 index 000000000..eb125d7dd --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java @@ -0,0 +1,361 @@ +/* + * 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.List; +import java.util.Map; +import java.util.regex.Pattern; + +import org.bson.Document; +import org.bson.types.ObjectId; + +import com.google.common.collect.Sets; +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. + * + * @author Mark Paluch + */ +abstract class MongodbDocumentSerializer implements Visitor { + + public Object handle(Expression expression) { + return expression.accept(this, null); + } + + public Document toSort(List> orderBys) { + Document sort = new Document(); + for (OrderSpecifier orderBy : orderBys) { + Object key = orderBy.getTarget().accept(this, null); + sort.append(key.toString(), orderBy.getOrder() == Order.ASC ? 1 : -1); + } + return sort; + } + + @Override + public Object visit(Constant expr, Void context) { + if (Enum.class.isAssignableFrom(expr.getType())) { + @SuppressWarnings("unchecked") // Guarded by previous check + Constant> expectedExpr = (Constant>) expr; + return expectedExpr.getConstant().name(); + } else { + return expr.getConstant(); + } + } + + @Override + public Object visit(TemplateExpression expr, Void context) { + throw new UnsupportedOperationException(); + } + + @Override + public Object visit(FactoryExpression expr, Void context) { + throw new UnsupportedOperationException(); + } + + protected String asDBKey(Operation expr, int index) { + return (String) asDBValue(expr, index); + } + + protected Object asDBValue(Operation expr, int index) { + return expr.getArg(index).accept(this, null); + } + + private String regexValue(Operation expr, int index) { + return Pattern.quote(expr.getArg(index).accept(this, null).toString()); + } + + protected Document asDocument(String key, 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)); + if (Sets.intersection(lhs.keySet(), rhs.keySet()).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), Pattern.compile("^" + regexValue(expr, 1))); + + } else if (op == Ops.STARTS_WITH_IC) { + return asDocument(asDBKey(expr, 0), Pattern.compile("^" + regexValue(expr, 1), Pattern.CASE_INSENSITIVE)); + + } else if (op == Ops.ENDS_WITH) { + return asDocument(asDBKey(expr, 0), Pattern.compile(regexValue(expr, 1) + "$")); + + } else if (op == Ops.ENDS_WITH_IC) { + return asDocument(asDBKey(expr, 0), Pattern.compile(regexValue(expr, 1) + "$", Pattern.CASE_INSENSITIVE)); + + } else if (op == Ops.EQ_IGNORE_CASE) { + return asDocument(asDBKey(expr, 0), Pattern.compile("^" + regexValue(expr, 1) + "$", Pattern.CASE_INSENSITIVE)); + + } else if (op == Ops.STRING_CONTAINS) { + return asDocument(asDBKey(expr, 0), Pattern.compile(".*" + regexValue(expr, 1) + ".*")); + + } else if (op == Ops.STRING_CONTAINS_IC) { + return asDocument(asDBKey(expr, 0), Pattern.compile(".*" + regexValue(expr, 1) + ".*", Pattern.CASE_INSENSITIVE)); + + } else if (op == Ops.MATCHES) { + return asDocument(asDBKey(expr, 0), Pattern.compile(asDBValue(expr, 1).toString())); + + } else if (op == Ops.MATCHES_IC) { + return asDocument(asDBKey(expr, 0), Pattern.compile(asDBValue(expr, 1).toString(), Pattern.CASE_INSENSITIVE)); + + } else if (op == Ops.LIKE) { + String regex = ExpressionUtils.likeToRegex((Expression) expr.getArg(1)).toString(); + return asDocument(asDBKey(expr, 0), Pattern.compile(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))); + } + + 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) { + 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(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/MongodbExpressions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbExpressions.java new file mode 100644 index 000000000..79fd892a3 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbExpressions.java @@ -0,0 +1,61 @@ +/* + * 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.Arrays; + +import com.querydsl.core.types.ConstantImpl; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.mongodb.MongodbOps; + +/** + * Mongodb Document-API-specific operations. + * + * @author tiwe + * @author Mark Paluch + */ +class MongodbExpressions { + + private MongodbExpressions() {} + + /** + * Finds the closest points relative to the given location and orders the results with decreasing proximity + * + * @param expr location + * @param latVal latitude + * @param longVal longitude + * @return predicate + */ + public static BooleanExpression near(Expression expr, double latVal, double longVal) { + return Expressions.booleanOperation(MongodbOps.NEAR, expr, ConstantImpl.create(Arrays.asList(latVal, longVal))); + } + + /** + * Finds the closest points relative to the given location on a sphere and orders the results with decreasing + * proximity + * + * @param expr location + * @param latVal latitude + * @param longVal longitude + * @return predicate + */ + public static BooleanExpression nearSphere(Expression expr, double latVal, double longVal) { + return Expressions.booleanOperation(MongodbOps.NEAR_SPHERE, expr, + ConstantImpl.create(Arrays.asList(latVal, longVal))); + } +}