blob: aed5c6dcee6a721d093acabe22b9ca118fd767a8 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.rya.mongodb.document.operators.query;
import static com.mongodb.assertions.Assertions.notNull;
import static java.util.Arrays.asList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import org.bson.Document;
import com.mongodb.QueryOperators;
import com.mongodb.lang.Nullable;
/**
* Utility for creating Document queries
*
* This is a {@link Document} based version of {@link com.mongodb.QueryBuilder}.
* @see com.mongodb.QueryBuilder
* @mongodb.driver.manual tutorial/query-documents/ Querying
*/
public class QueryBuilder {
private final Document query;
private String currentKey;
private boolean hasNot;
/**
* Creates a builder with an empty query
*/
public QueryBuilder() {
query = new Document();
}
/**
* Returns a new QueryBuilder.
*
* @return a builder
*/
public static QueryBuilder start() {
return new QueryBuilder();
}
/**
* Creates a new query with a document key
*
* @param key MongoDB document key
* @return {@code this}
*/
public static QueryBuilder start(final String key) {
return (new QueryBuilder()).put(key);
}
/**
* Adds a new key to the query if not present yet. Sets this key as the current key.
*
* @param key MongoDB document key
* @return {@code this}
*/
public QueryBuilder put(final String key) {
currentKey = key;
if (query.get(key) == null) {
query.put(currentKey, new NullObject());
}
return this;
}
/**
* Equivalent to {@code QueryBuilder.put(key)}. Intended for compound query chains to be more readable, e.g. {@code
* QueryBuilder.start("a").greaterThan(1).and("b").lessThan(3) }
*
* @param key MongoDB document key
* @return {@code this}
*/
public QueryBuilder and(final String key) {
return put(key);
}
/**
* Equivalent to the $gt operator
*
* @param object Value to query
* @return {@code this}
*/
public QueryBuilder greaterThan(final Object object) {
addOperand(QueryOperators.GT, object);
return this;
}
/**
* Equivalent to the $gte operator
*
* @param object Value to query
* @return {@code this}
*/
public QueryBuilder greaterThanEquals(final Object object) {
addOperand(QueryOperators.GTE, object);
return this;
}
/**
* Equivalent to the $lt operand
*
* @param object Value to query
* @return {@code this}
*/
public QueryBuilder lessThan(final Object object) {
addOperand(QueryOperators.LT, object);
return this;
}
/**
* Equivalent to the $lte operand
*
* @param object Value to query
* @return {@code this}
*/
public QueryBuilder lessThanEquals(final Object object) {
addOperand(QueryOperators.LTE, object);
return this;
}
/**
* Equivalent of the find({key:value})
*
* @param object Value to query
* @return {@code this}
*/
public QueryBuilder is(final Object object) {
addOperand(null, object);
return this;
}
/**
* Equivalent of the $ne operand
*
* @param object Value to query
* @return {@code this}
*/
public QueryBuilder notEquals(final Object object) {
addOperand(QueryOperators.NE, object);
return this;
}
/**
* Equivalent of the $in operand
*
* @param object Value to query
* @return {@code this}
*/
public QueryBuilder in(final Object object) {
addOperand(QueryOperators.IN, object);
return this;
}
/**
* Equivalent of the $nin operand
*
* @param object Value to query
* @return {@code this}
*/
public QueryBuilder notIn(final Object object) {
addOperand(QueryOperators.NIN, object);
return this;
}
/**
* Equivalent of the $mod operand
*
* @param object Value to query
* @return {@code this}
*/
public QueryBuilder mod(final Object object) {
addOperand(QueryOperators.MOD, object);
return this;
}
/**
* Equivalent of the $all operand
*
* @param object Value to query
* @return {@code this}
*/
public QueryBuilder all(final Object object) {
addOperand(QueryOperators.ALL, object);
return this;
}
/**
* Equivalent of the $size operand
*
* @param object Value to query
* @return {@code this}
*/
public QueryBuilder size(final Object object) {
addOperand(QueryOperators.SIZE, object);
return this;
}
/**
* Equivalent of the $exists operand
*
* @param object Value to query
* @return {@code this}
*/
public QueryBuilder exists(final Object object) {
addOperand(QueryOperators.EXISTS, object);
return this;
}
/**
* Passes a regular expression for a query
*
* @param regex Regex pattern object
* @return {@code this}
*/
public QueryBuilder regex(final Pattern regex) {
addOperand(null, regex);
return this;
}
/**
* Equivalent to the $elemMatch operand
*
* @param match the object to match
* @return {@code this}
*/
public QueryBuilder elemMatch(final Document match) {
addOperand(QueryOperators.ELEM_MATCH, match);
return this;
}
/**
* Equivalent of the $within operand, used for geospatial operation
*
* @param x x coordinate
* @param y y coordinate
* @param radius radius
* @return {@code this}
*/
public QueryBuilder withinCenter(final double x, final double y, final double radius) {
addOperand(QueryOperators.WITHIN,
new Document(QueryOperators.CENTER, asList(asList(x, y), radius)));
return this;
}
/**
* Equivalent of the $near operand
*
* @param x x coordinate
* @param y y coordinate
* @return {@code this}
*/
public QueryBuilder near(final double x, final double y) {
addOperand(QueryOperators.NEAR,
asList(x, y));
return this;
}
/**
* Equivalent of the $near operand
*
* @param x x coordinate
* @param y y coordinate
* @param maxDistance max distance
* @return {@code this}
*/
public QueryBuilder near(final double x, final double y, final double maxDistance) {
addOperand(QueryOperators.NEAR,
asList(x, y));
addOperand(QueryOperators.MAX_DISTANCE,
maxDistance);
return this;
}
/**
* Equivalent of the $nearSphere operand
*
* @param longitude coordinate in decimal degrees
* @param latitude coordinate in decimal degrees
* @return {@code this}
*/
public QueryBuilder nearSphere(final double longitude, final double latitude) {
addOperand(QueryOperators.NEAR_SPHERE,
asList(longitude, latitude));
return this;
}
/**
* Equivalent of the $nearSphere operand
*
* @param longitude coordinate in decimal degrees
* @param latitude coordinate in decimal degrees
* @param maxDistance max spherical distance
* @return {@code this}
*/
public QueryBuilder nearSphere(final double longitude, final double latitude, final double maxDistance) {
addOperand(QueryOperators.NEAR_SPHERE,
asList(longitude, latitude));
addOperand(QueryOperators.MAX_DISTANCE,
maxDistance);
return this;
}
/**
* Equivalent of the $centerSphere operand mostly intended for queries up to a few hundred miles or km.
*
* @param longitude coordinate in decimal degrees
* @param latitude coordinate in decimal degrees
* @param maxDistance max spherical distance
* @return {@code this}
*/
public QueryBuilder withinCenterSphere(final double longitude, final double latitude, final double maxDistance) {
addOperand(QueryOperators.WITHIN,
new Document(QueryOperators.CENTER_SPHERE,
asList(asList(longitude, latitude), maxDistance)));
return this;
}
/**
* Equivalent to a $within operand, based on a bounding box using represented by two corners
*
* @param x the x coordinate of the first box corner.
* @param y the y coordinate of the first box corner.
* @param x2 the x coordinate of the second box corner.
* @param y2 the y coordinate of the second box corner.
* @return {@code this}
*/
public QueryBuilder withinBox(final double x, final double y, final double x2, final double y2) {
addOperand(QueryOperators.WITHIN,
new Document(QueryOperators.BOX, new Object[]{new Double[]{x, y}, new Double[]{x2, y2}}));
return this;
}
/**
* Equivalent to a $within operand, based on a bounding polygon represented by an array of points
*
* @param points an array of Double[] defining the vertices of the search area
* @return {@code this}
*/
public QueryBuilder withinPolygon(final List<Double[]> points) {
notNull("points", points);
if (points.isEmpty() || points.size() < 3) {
throw new IllegalArgumentException("Polygon insufficient number of vertices defined");
}
addOperand(QueryOperators.WITHIN,
new Document(QueryOperators.POLYGON, convertToListOfLists(points)));
return this;
}
private List<List<Double>> convertToListOfLists(final List<Double[]> points) {
final List<List<Double>> listOfLists = new ArrayList<List<Double>>(points.size());
for (final Double[] cur : points) {
final List<Double> list = new ArrayList<Double>(cur.length);
Collections.addAll(list, cur);
listOfLists.add(list);
}
return listOfLists;
}
/**
* Equivalent to a $text operand.
*
* @param search the search terms to apply to the text index.
* @return {@code this}
* @mongodb.server.release 2.6
*/
public QueryBuilder text(final String search) {
return text(search, null);
}
/**
* Equivalent to a $text operand.
*
* @param search the search terms to apply to the text index.
* @param language the language to use.
* @return {@code this}
* @mongodb.server.release 2.6
*/
public QueryBuilder text(final String search, @Nullable final String language) {
if (currentKey != null) {
throw new QueryBuilderException("The text operand may only occur at the top-level of a query. It does"
+ " not apply to a specific element, but rather to a document as a whole.");
}
put(QueryOperators.TEXT);
addOperand(QueryOperators.SEARCH, search);
if (language != null) {
addOperand(QueryOperators.LANGUAGE, language);
}
return this;
}
/**
* Equivalent to $not meta operator. Must be followed by an operand, not a value, e.g. {@code
* QueryBuilder.start("val").not().mod(Arrays.asList(10, 1)) }
*
* @return {@code this}
*/
public QueryBuilder not() {
hasNot = true;
return this;
}
/**
* Equivalent to an $or operand
*
* @param ors the list of conditions to or together
* @return {@code this}
*/
public QueryBuilder or(final Document... ors) {
List<Object> l = query.getList(QueryOperators.OR, Object.class);
if (l == null) {
l = new ArrayList<>();
query.put(QueryOperators.OR, l);
}
Collections.addAll(l, ors);
return this;
}
/**
* Equivalent to an $and operand
*
* @param ands the list of conditions to and together
* @return {@code this}
*/
public QueryBuilder and(final Document... ands) {
List<Object> l = query.getList(QueryOperators.AND, Object.class);
if (l == null) {
l = new ArrayList<>();
query.put(QueryOperators.AND, l);
}
Collections.addAll(l, ands);
return this;
}
/**
* Creates a {@code Document} query to be used for the driver's find operations
*
* @return {@code this}
* @throws RuntimeException if a key does not have a matching operand
*/
public Document get() {
for (final String key : query.keySet()) {
if (query.get(key) instanceof NullObject) {
throw new QueryBuilderException("No operand for key:" + key);
}
}
return query;
}
private void addOperand(@Nullable final String op, final Object value) {
Object valueToPut = value;
if (op == null) {
if (hasNot) {
valueToPut = new Document(QueryOperators.NOT, valueToPut);
hasNot = false;
}
query.put(currentKey, valueToPut);
return;
}
final Object storedValue = query.get(currentKey);
Document operand;
if (!(storedValue instanceof Document)) {
operand = new Document();
if (hasNot) {
final Document notOperand = new Document(QueryOperators.NOT, operand);
query.put(currentKey, notOperand);
hasNot = false;
} else {
query.put(currentKey, operand);
}
} else {
operand = (Document) query.get(currentKey);
if (operand.get(QueryOperators.NOT) != null) {
operand = (Document) operand.get(QueryOperators.NOT);
}
}
operand.put(op, valueToPut);
}
static class QueryBuilderException extends RuntimeException {
private static final long serialVersionUID = 1L;
QueryBuilderException(final String message) {
super(message);
}
}
private static class NullObject {
}
}