/*
* Copyright 2010-2017 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.core.query;
import static org.springframework.data.mongodb.core.query.SerializationUtils.*;
import static org.springframework.util.ObjectUtils.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.bson.Document;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
import org.springframework.data.mongodb.core.Collation;
import org.springframework.util.Assert;
/**
* @author Thomas Risberg
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
* @author Mark Paluch
*/
public class Query {
private static final String RESTRICTED_TYPES_KEY = "_$RESTRICTED_TYPES";
private final Set<Class<?>> restrictedTypes = new HashSet<>();
private final Map<String, CriteriaDefinition> criteria = new LinkedHashMap<>();
private Field fieldSpec = null;
private Sort sort = Sort.unsorted();
private long skip;
private int limit;
private String hint;
private Meta meta = new Meta();
private Optional<Collation> collation = Optional.empty();
/**
* Static factory method to create a {@link Query} using the provided {@link CriteriaDefinition}.
*
* @param criteriaDefinition must not be {@literal null}.
* @return
* @since 1.6
*/
public static Query query(CriteriaDefinition criteriaDefinition) {
return new Query(criteriaDefinition);
}
public Query() {}
/**
* Creates a new {@link Query} using the given {@link CriteriaDefinition}.
*
* @param criteriaDefinition must not be {@literal null}.
* @since 1.6
*/
public Query(CriteriaDefinition criteriaDefinition) {
addCriteria(criteriaDefinition);
}
/**
* Adds the given {@link CriteriaDefinition} to the current {@link Query}.
*
* @param criteriaDefinition must not be {@literal null}.
* @return
* @since 1.6
*/
public Query addCriteria(CriteriaDefinition criteriaDefinition) {
CriteriaDefinition existing = this.criteria.get(criteriaDefinition.getKey());
String key = criteriaDefinition.getKey();
if (existing == null) {
this.criteria.put(key, criteriaDefinition);
} else {
throw new InvalidMongoDbApiUsageException(
String.format("Due to limitations of the com.mongodb.BasicDocument, you can't add a second '%s' criteria. "
+ "Query already contains '%s'", key, serializeToJsonSafely(existing.getCriteriaObject())));
}
return this;
}
public Field fields() {
if (this.fieldSpec == null) {
this.fieldSpec = new Field();
}
return this.fieldSpec;
}
/**
* Set number of documents to skip before returning results.
*
* @param skip
* @return
*/
public Query skip(long skip) {
this.skip = skip;
return this;
}
/**
* Limit the number of returned documents to {@code limit}.
*
* @param limit
* @return
*/
public Query limit(int limit) {
this.limit = limit;
return this;
}
/**
* Configures the query to use the given hint when being executed.
*
* @param name must not be {@literal null} or empty.
* @return
*/
public Query withHint(String name) {
Assert.hasText(name, "Hint must not be empty or null!");
this.hint = name;
return this;
}
/**
* Sets the given pagination information on the {@link Query} instance. Will transparently set {@code skip} and
* {@code limit} as well as applying the {@link Sort} instance defined with the {@link Pageable}.
*
* @param pageable
* @return
*/
public Query with(Pageable pageable) {
if (pageable.isUnpaged()) {
return this;
}
this.limit = pageable.getPageSize();
this.skip = pageable.getOffset();
return with(pageable.getSort());
}
/**
* Adds a {@link Sort} to the {@link Query} instance.
*
* @param sort
* @return
*/
public Query with(Sort sort) {
Assert.notNull(sort, "Sort must not be null!");
if (sort.isUnsorted()) {
return this;
}
sort.stream().filter(Order::isIgnoreCase).findFirst().ifPresent(it -> {
throw new IllegalArgumentException(String.format("Given sort contained an Order for %s with ignore case! "
+ "MongoDB does not support sorting ignoring case currently!", it.getProperty()));
});
this.sort = this.sort.and(sort);
return this;
}
/**
* @return the restrictedTypes
*/
public Set<Class<?>> getRestrictedTypes() {
return restrictedTypes == null ? Collections.emptySet() : restrictedTypes;
}
/**
* Restricts the query to only return documents instances that are exactly of the given types.
*
* @param type may not be {@literal null}
* @param additionalTypes may not be {@literal null}
* @return
*/
public Query restrict(Class<?> type, Class<?>... additionalTypes) {
Assert.notNull(type, "Type must not be null!");
Assert.notNull(additionalTypes, "AdditionalTypes must not be null");
restrictedTypes.add(type);
for (Class<?> additionalType : additionalTypes) {
restrictedTypes.add(additionalType);
}
return this;
}
public Document getQueryObject() {
Document document = new Document();
for (CriteriaDefinition definition : criteria.values()) {
document.putAll(definition.getCriteriaObject());
}
if (!restrictedTypes.isEmpty()) {
document.put(RESTRICTED_TYPES_KEY, getRestrictedTypes());
}
return document;
}
public Document getFieldsObject() {
return this.fieldSpec == null ? null : fieldSpec.getFieldsObject();
}
public Document getSortObject() {
if (this.sort.isUnsorted()) {
return null;
}
Document document = new Document();
this.sort.stream()//
.forEach(order -> document.put(order.getProperty(), order.isAscending() ? 1 : -1));
return document;
}
/**
* Get the number of documents to skip.
*
* @return
*/
public long getSkip() {
return this.skip;
}
/**
* Get the maximum number of documents to be return.
*
* @return
*/
public int getLimit() {
return this.limit;
}
/**
* @return
*/
public String getHint() {
return hint;
}
/**
* @param maxTimeMsec
* @return
* @see Meta#setMaxTimeMsec(long)
* @since 1.6
*/
public Query maxTimeMsec(long maxTimeMsec) {
meta.setMaxTimeMsec(maxTimeMsec);
return this;
}
/**
* @param timeout
* @param timeUnit
* @return
* @see Meta#setMaxTime(long, TimeUnit)
* @since 1.6
*/
public Query maxTime(long timeout, TimeUnit timeUnit) {
meta.setMaxTime(timeout, timeUnit);
return this;
}
/**
* @param maxScan
* @return
* @see Meta#setMaxScan(long)
* @since 1.6
*/
public Query maxScan(long maxScan) {
meta.setMaxScan(maxScan);
return this;
}
/**
* @param comment
* @return
* @see Meta#setComment(String)
* @since 1.6
*/
public Query comment(String comment) {
meta.setComment(comment);
return this;
}
/**
* @return
* @see Meta#setSnapshot(boolean)
* @since 1.6
*/
public Query useSnapshot() {
meta.setSnapshot(true);
return this;
}
/**
* @return
* @see org.springframework.data.mongodb.core.query.Meta.CursorOption#NO_TIMEOUT
* @since 1.10
*/
public Query noCursorTimeout() {
meta.addFlag(Meta.CursorOption.NO_TIMEOUT);
return this;
}
/**
* @return
* @see org.springframework.data.mongodb.core.query.Meta.CursorOption#EXHAUST
* @since 1.10
*/
public Query exhaust() {
meta.addFlag(Meta.CursorOption.EXHAUST);
return this;
}
/**
* @return
* @see org.springframework.data.mongodb.core.query.Meta.CursorOption#SLAVE_OK
* @since 1.10
*/
public Query slaveOk() {
meta.addFlag(Meta.CursorOption.SLAVE_OK);
return this;
}
/**
* @return
* @see org.springframework.data.mongodb.core.query.Meta.CursorOption#PARTIAL
* @since 1.10
*/
public Query partialResults() {
meta.addFlag(Meta.CursorOption.PARTIAL);
return this;
}
/**
* @return never {@literal null}.
* @since 1.6
*/
public Meta getMeta() {
return meta;
}
/**
* @param meta must not be {@literal null}.
* @since 1.6
*/
public void setMeta(Meta meta) {
Assert.notNull(meta, "Query meta might be empty but must not be null.");
this.meta = meta;
}
/**
* Set the {@link Collation} applying language-specific rules for string comparison.
*
* @param collation can be {@literal null}.
* @return
* @since 2.0
*/
public Query collation(Collation collation) {
this.collation = Optional.ofNullable(collation);
return this;
}
/**
* Get the {@link Collation} defining language-specific rules for string comparison.
*
* @return
* @since 2.0
*/
public Optional<Collation> getCollation() {
return collation;
}
protected List<CriteriaDefinition> getCriteria() {
return new ArrayList<>(this.criteria.values());
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return String.format("Query: %s, Fields: %s, Sort: %s", serializeToJsonSafely(getQueryObject()),
serializeToJsonSafely(getFieldsObject()), serializeToJsonSafely(getSortObject()));
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || !getClass().equals(obj.getClass())) {
return false;
}
return querySettingsEquals((Query) obj);
}
/**
* Tests whether the settings of the given {@link Query} are equal to this query.
*
* @param that
* @return
*/
protected boolean querySettingsEquals(Query that) {
boolean criteriaEqual = this.criteria.equals(that.criteria);
boolean fieldsEqual = nullSafeEquals(this.fieldSpec, that.fieldSpec);
boolean sortEqual = this.sort.equals(that.sort);
boolean hintEqual = nullSafeEquals(this.hint, that.hint);
boolean skipEqual = this.skip == that.skip;
boolean limitEqual = this.limit == that.limit;
boolean metaEqual = nullSafeEquals(this.meta, that.meta);
boolean collationEqual = nullSafeEquals(this.collation.orElse(null), that.collation.orElse(null));
return criteriaEqual && fieldsEqual && sortEqual && hintEqual && skipEqual && limitEqual && metaEqual
&& collationEqual;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int result = 17;
result += 31 * criteria.hashCode();
result += 31 * nullSafeHashCode(fieldSpec);
result += 31 * nullSafeHashCode(sort);
result += 31 * nullSafeHashCode(hint);
result += 31 * skip;
result += 31 * limit;
result += 31 * nullSafeHashCode(meta);
result += 31 * nullSafeHashCode(collation.orElse(null));
return result;
}
/**
* Returns whether the given key is the one used to hold the type restriction information.
*
* @deprecated don't call this method as the restricted type handling will undergo some significant changes going
* forward.
* @param key
* @return
*/
@Deprecated
public static boolean isRestrictedTypeKey(String key) {
return RESTRICTED_TYPES_KEY.equals(key);
}
}