DATAMONGO-1848 - Import Document-based Querydsl support.
Original Pull Request: #579
This commit is contained in:
committed by
Christoph Strobl
parent
0ec82e1f2e
commit
b7755e71f6
@@ -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 <Q> concrete subtype
|
||||
*/
|
||||
abstract class AbstractMongodbQuery<Q extends AbstractMongodbQuery<Q>> implements SimpleQuery<Q> {
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
static class NoResults extends RuntimeException {}
|
||||
|
||||
private final MongodbDocumentSerializer serializer;
|
||||
private final QueryMixin<Q> 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<Q>(query, new DefaultQueryMetadata(), false);
|
||||
this.serializer = serializer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a join
|
||||
*
|
||||
* @param ref reference
|
||||
* @param target join target
|
||||
* @return join builder
|
||||
*/
|
||||
public <T> JoinBuilder<Q, T> join(Path<T> ref, Path<T> target) {
|
||||
return new JoinBuilder<Q, T>(queryMixin, ref, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a join
|
||||
*
|
||||
* @param ref reference
|
||||
* @param target join target
|
||||
* @return join builder
|
||||
*/
|
||||
public <T> JoinBuilder<Q, T> join(CollectionPathBase<?, T, ?> ref, Path<T> target) {
|
||||
return new JoinBuilder<Q, T>(queryMixin, ref, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a constraint for an embedded object
|
||||
*
|
||||
* @param collection collection
|
||||
* @param target target
|
||||
* @return builder
|
||||
*/
|
||||
public <T> AnyEmbeddedBuilder<Q> anyEmbedded(Path<? extends Collection<T>> collection, Path<T> target) {
|
||||
return new AnyEmbeddedBuilder<Q>(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 <T> Q set(ParamExpression<T> 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<Q> 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();
|
||||
}
|
||||
}
|
||||
@@ -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 <Q> query type
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class AnyEmbeddedBuilder<Q extends AbstractMongodbQuery<Q>> {
|
||||
|
||||
private final QueryMixin<Q> queryMixin;
|
||||
|
||||
private final Path<? extends Collection<?>> collection;
|
||||
|
||||
public AnyEmbeddedBuilder(QueryMixin<Q> queryMixin, Path<? extends Collection<?>> collection) {
|
||||
this.queryMixin = queryMixin;
|
||||
this.collection = collection;
|
||||
}
|
||||
|
||||
public Q on(Predicate... conditions) {
|
||||
return queryMixin
|
||||
.where(ExpressionUtils.predicate(MongodbOps.ELEM_MATCH, collection, ExpressionUtils.allOf(conditions)));
|
||||
}
|
||||
}
|
||||
@@ -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 <K> result type
|
||||
* @param <Q> concrete subtype
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class FetchableMongodbQuery<K> extends AbstractMongodbQuery<FetchableMongodbQuery<K>> implements Fetchable<K> {
|
||||
|
||||
private final Class<K> entityClass;
|
||||
private final String collection;
|
||||
private final MongoOperations mongoOperations;
|
||||
|
||||
public FetchableMongodbQuery(MongodbDocumentSerializer serializer, Class<? extends K> entityClass,
|
||||
MongoOperations mongoOperations) {
|
||||
|
||||
super(serializer);
|
||||
|
||||
this.entityClass = (Class<K>) entityClass;
|
||||
this.collection = mongoOperations.getCollectionName(entityClass);
|
||||
this.mongoOperations = mongoOperations;
|
||||
}
|
||||
|
||||
public FetchableMongodbQuery(MongodbDocumentSerializer serializer, Class<? extends K> entityClass, String collection,
|
||||
MongoOperations mongoOperations) {
|
||||
|
||||
super(serializer);
|
||||
|
||||
this.entityClass = (Class<K>) entityClass;
|
||||
this.collection = collection;
|
||||
this.mongoOperations = mongoOperations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate with the specific fields
|
||||
*
|
||||
* @param paths fields to return
|
||||
* @return iterator
|
||||
*/
|
||||
public CloseableIterator<K> iterate(Path<?>... paths) {
|
||||
getQueryMixin().setProjection(paths);
|
||||
return iterate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CloseableIterator<K> iterate() {
|
||||
|
||||
org.springframework.data.util.CloseableIterator<? extends K> stream = mongoOperations.stream(createQuery(),
|
||||
entityClass, collection);
|
||||
|
||||
return new CloseableIterator<K>() {
|
||||
@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<K> fetch(Path<?>... paths) {
|
||||
getQueryMixin().setProjection(paths);
|
||||
return fetch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<K> 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<K> fetchResults(Path<?>... paths) {
|
||||
getQueryMixin().setProjection(paths);
|
||||
return fetchResults();
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryResults<K> 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<OrderSpecifier<?>> 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<Expression<?>, Predicate> predicates = HashMultimap.create();
|
||||
List<JoinExpression> 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<Predicate> extraFilters = predicates.get(target.getRoot());
|
||||
Predicate filter = ExpressionUtils.allOf(join.getCondition(), allOf(extraFilters));
|
||||
List<? extends Object> 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<Object>) path, ids));
|
||||
}
|
||||
Path<?> source = (Path) ((Operation) joins.get(0).getTarget()).getArg(0);
|
||||
return allOf(predicates.get(source.getRoot()));
|
||||
}
|
||||
|
||||
private Predicate allOf(Collection<Predicate> predicates) {
|
||||
return predicates != null ? ExpressionUtils.allOf(predicates) : null;
|
||||
}
|
||||
|
||||
protected List<Object> 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);
|
||||
}
|
||||
}
|
||||
@@ -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 <Q>
|
||||
* @param <T>
|
||||
*/
|
||||
class JoinBuilder<Q extends AbstractMongodbQuery<Q>, T> {
|
||||
|
||||
private final QueryMixin<Q> queryMixin;
|
||||
|
||||
private final Path<?> ref;
|
||||
|
||||
private final Path<T> target;
|
||||
|
||||
public JoinBuilder(QueryMixin<Q> queryMixin, Path<?> ref, Path<T> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<Object, Void> {
|
||||
|
||||
public Object handle(Expression<?> expression) {
|
||||
return expression.accept(this, null);
|
||||
}
|
||||
|
||||
public Document toSort(List<OrderSpecifier<?>> 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<? extends Enum<?>> expectedExpr = (Constant<? extends Enum<?>>) expr;
|
||||
return expectedExpr.getConstant().name();
|
||||
} else {
|
||||
return expr.getConstant();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object visit(TemplateExpression<?> expr, Void context) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object visit(FactoryExpression<?> expr, Void context) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
protected String asDBKey(Operation<?> expr, int index) {
|
||||
return (String) asDBValue(expr, index);
|
||||
}
|
||||
|
||||
protected Object asDBValue(Operation<?> expr, int index) {
|
||||
return expr.getArg(index).accept(this, null);
|
||||
}
|
||||
|
||||
private String regexValue(Operation<?> expr, int index) {
|
||||
return Pattern.quote(expr.getArg(index).accept(this, null).toString());
|
||||
}
|
||||
|
||||
protected 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<Object, Object> lhs = (Map<Object, Object>) handle(expr.getArg(0));
|
||||
Map<Object, Object> rhs = (Map<Object, Object>) handle(expr.getArg(1));
|
||||
if (Sets.intersection(lhs.keySet(), rhs.keySet()).isEmpty()) {
|
||||
lhs.putAll(rhs);
|
||||
return lhs;
|
||||
} else {
|
||||
List<Object> list = new ArrayList<Object>(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<Object> list = new ArrayList<Object>(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<? extends Collection<?>>) 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<? extends Collection<?>>) 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<Object> list = new ArrayList<Object>(2);
|
||||
list.add(asDocument(asDBKey(expr, 0), new ArrayList<Object>()));
|
||||
list.add(asDocument(asDBKey(expr, 0), asDocument("$exists", false)));
|
||||
return asDocument("$or", list);
|
||||
|
||||
} 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<Object> list = new ArrayList<Object>();
|
||||
for (Map.Entry<String, Object> entry : arg.entrySet()) {
|
||||
if (entry.getKey().equals("$or")) {
|
||||
list.add(asDocument("$nor", entry.getValue()));
|
||||
|
||||
} else if (entry.getKey().equals("$and")) {
|
||||
List<Object> list2 = new ArrayList<Object>();
|
||||
for (Object o : ((Collection) entry.getValue())) {
|
||||
list2.add(negate((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<Object> list2 = new ArrayList<Object>();
|
||||
for (Map.Entry<String, Object> entry2 : value.entrySet()) {
|
||||
list2.add(asDocument(key, asDocument("$not", asDocument(entry2.getKey(), entry2.getValue()))));
|
||||
}
|
||||
return asDocument("$or", list2);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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<Double[]> 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<Double[]> expr, double latVal, double longVal) {
|
||||
return Expressions.booleanOperation(MongodbOps.NEAR_SPHERE, expr,
|
||||
ConstantImpl.create(Arrays.asList(latVal, longVal)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user