1 /*
2 * #%L
3 * QueryBuilder.java - mongodb-async-driver - Allanbank Consulting, Inc.
4 * %%
5 * Copyright (C) 2011 - 2014 Allanbank Consulting, Inc.
6 * %%
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 * #L%
19 */
20
21 package com.allanbank.mongodb.builder;
22
23 import java.util.HashSet;
24 import java.util.Iterator;
25 import java.util.LinkedHashMap;
26 import java.util.Map;
27 import java.util.Set;
28
29 import com.allanbank.mongodb.bson.Document;
30 import com.allanbank.mongodb.bson.DocumentAssignable;
31 import com.allanbank.mongodb.bson.Element;
32 import com.allanbank.mongodb.bson.builder.ArrayBuilder;
33 import com.allanbank.mongodb.bson.builder.BuilderFactory;
34 import com.allanbank.mongodb.bson.builder.DocumentBuilder;
35 import com.allanbank.mongodb.bson.element.DocumentElement;
36 import com.allanbank.mongodb.bson.element.StringElement;
37 import com.allanbank.mongodb.bson.impl.EmptyDocument;
38
39 /**
40 * QueryBuilder provides support for constructing queries. Most users are
41 * expected to use the static methods of this class to create query
42 * {@link Document}s.
43 * <p>
44 * As an example:<blockquote>
45 *
46 * <pre>
47 * <code>
48 *
49 * import static {@link com.allanbank.mongodb.builder.QueryBuilder#and com.allanbank.mongodb.builder.QueryBuilder.and}
50 * import static {@link com.allanbank.mongodb.builder.QueryBuilder#or com.allanbank.mongodb.builder.QueryBuilder.or}
51 * import static {@link com.allanbank.mongodb.builder.QueryBuilder#not com.allanbank.mongodb.builder.QueryBuilder.not}
52 * import static {@link com.allanbank.mongodb.builder.QueryBuilder#where com.allanbank.mongodb.builder.QueryBuilder.where}
53 *
54 * Document query =
55 * or(
56 * where("f").greaterThan(23).lessThan(42).and("g").lessThan(3),
57 * and(
58 * where("f").greaterThanOrEqualTo(42),
59 * not( where("g").lessThan(3) )
60 * )
61 * );
62 * </code>
63 * </pre>
64 *
65 * </blockquote>
66 *
67 * @api.yes This class is part of the driver's API. Public and protected members
68 * will be deprecated for at least 1 non-bugfix release (version
69 * numbers are <major>.<minor>.<bugfix>) before being
70 * removed or modified.
71 * @copyright 2012-2013, Allanbank Consulting, Inc., All Rights Reserved
72 */
73 public class QueryBuilder implements DocumentAssignable {
74
75 /**
76 * Creates a single document that is the conjunction of the criteria
77 * provided.
78 *
79 * @param criteria
80 * The criteria to create a conjunction of.
81 * @return The conjunction Document.
82 */
83 public static Document and(final DocumentAssignable... criteria) {
84 if (criteria.length <= 0) {
85 return EmptyDocument.INSTANCE;
86 }
87 else if (criteria.length == 1) {
88 return criteria[0].asDocument();
89 }
90 else {
91 // Perform 2 things at once.
92 // 1) Build the $and document.
93 // 2) Build a flat document to optimize the $and away if none of
94 // the nested elements collide.
95 final Set<String> seen = new HashSet<String>();
96 DocumentBuilder optimized = BuilderFactory.start();
97 final DocumentBuilder docBuilder = BuilderFactory.start();
98 final ArrayBuilder arrayBuilder = docBuilder
99 .pushArray(LogicalOperator.AND.getToken());
100
101 for (final DocumentAssignable criterion : criteria) {
102 final Document subQuery = criterion.asDocument();
103 // Make sure at least 1 element.
104 final Iterator<Element> iter = subQuery.iterator();
105 if (iter.hasNext()) {
106 arrayBuilder.addDocument(subQuery);
107
108 while ((optimized != null) && iter.hasNext()) {
109 final Element subQueryElement = iter.next();
110 if (seen.add(subQueryElement.getName())) {
111 optimized.add(subQueryElement);
112 }
113 else {
114 optimized = null;
115 }
116 }
117 }
118 }
119
120 if (optimized != null) {
121 return optimized.build();
122 }
123 return docBuilder.build();
124 }
125 }
126
127 /**
128 * Creates a single document that is the disjunction of the criteria
129 * provided.
130 *
131 * @param criteria
132 * The criteria to create a disjunction of.
133 * @return The disjunction Document.
134 */
135 public static Document nor(final DocumentAssignable... criteria) {
136
137 final DocumentBuilder docBuilder = BuilderFactory.start();
138 final ArrayBuilder arrayBuilder = docBuilder
139 .pushArray(LogicalOperator.NOR.getToken());
140
141 for (final DocumentAssignable criterion : criteria) {
142 final Document subQuery = criterion.asDocument();
143 if (subQuery.iterator().hasNext()) {
144 arrayBuilder.addDocument(subQuery);
145 }
146 }
147
148 return docBuilder.build();
149 }
150
151 /**
152 * Negate a set of criteria.
153 *
154 * @param criteria
155 * The criteria to negate. These will normally be
156 * {@link ConditionBuilder}s or {@link Document}s.
157 * @return The negated criteria.
158 */
159 public static Document not(final DocumentAssignable... criteria) {
160 final DocumentBuilder docBuilder = BuilderFactory.start();
161 final ArrayBuilder arrayBuilder = docBuilder
162 .pushArray(LogicalOperator.NOT.getToken());
163
164 for (final DocumentAssignable criterion : criteria) {
165 final Document subQuery = criterion.asDocument();
166 if (subQuery.iterator().hasNext()) {
167 arrayBuilder.addDocument(subQuery);
168 }
169 }
170
171 return docBuilder.build();
172 }
173
174 /**
175 * Creates a single document that is the disjunction of the criteria
176 * provided.
177 *
178 * @param criteria
179 * The criteria to create a disjunction of.
180 * @return The disjunction Document.
181 */
182 public static Document or(final DocumentAssignable... criteria) {
183 if (criteria.length <= 0) {
184 return EmptyDocument.INSTANCE;
185 }
186 else if (criteria.length == 1) {
187 return criteria[0].asDocument();
188 }
189 else {
190 final DocumentBuilder docBuilder = BuilderFactory.start();
191 final ArrayBuilder arrayBuilder = docBuilder
192 .pushArray(LogicalOperator.OR.getToken());
193
194 for (final DocumentAssignable criterion : criteria) {
195 final Document subQuery = criterion.asDocument();
196 if (subQuery.iterator().hasNext()) {
197 arrayBuilder.addDocument(subQuery);
198 }
199 }
200
201 return docBuilder.build();
202 }
203 }
204
205 /**
206 * Start a criteria for a single conjunctions.
207 *
208 * @param field
209 * The field to start the criteria against.
210 * @return A {@link ConditionBuilder} for constructing the conditions.
211 */
212 public static ConditionBuilder where(final String field) {
213 return new QueryBuilder().whereField(field);
214 }
215
216 /** The set of conditions created for the query. */
217 private final Map<String, ConditionBuilder> myConditions;
218
219 /** The comment for the query. */
220 private String myQueryComment;
221
222 /** The text search expression. */
223 private Element myTextQuery;
224
225 /** The ad-hoc JavaScript condition. */
226 private String myWhere;
227
228 /**
229 * Creates a new QueryBuilder.
230 */
231 public QueryBuilder() {
232 myConditions = new LinkedHashMap<String, ConditionBuilder>();
233
234 reset();
235 }
236
237 /**
238 * {@inheritDoc}
239 * <p>
240 * Returns the result of {@link #build()}.
241 * </p>
242 *
243 * @see #build()
244 */
245 @Override
246 public Document asDocument() {
247 return build();
248 }
249
250 /**
251 * Construct the final query document.
252 *
253 * @return The document containing the constraints specified.
254 */
255 public Document build() {
256 final DocumentBuilder builder = BuilderFactory.start();
257
258 if (myQueryComment != null) {
259 builder.add(MiscellaneousOperator.COMMENT.getToken(),
260 myQueryComment);
261 }
262
263 if (myTextQuery != null) {
264 builder.add(myTextQuery);
265 }
266
267 for (final ConditionBuilder condBuilder : myConditions.values()) {
268 final Element condElement = condBuilder.buildFieldCondition();
269
270 if (condElement != null) {
271 builder.add(condElement);
272 }
273 }
274
275 if (myWhere != null) {
276 builder.addJavaScript(MiscellaneousOperator.WHERE.getToken(),
277 myWhere);
278 }
279
280 return builder.build();
281 }
282
283 /**
284 * Adds a comment to the query builder. Comments are useful for locating
285 * queries in the profiler log within MongoDB.
286 * <p>
287 * Only a single {@link #comment} can be used. Calling multiple
288 * <tt>comment(...)</tt> methods overwrites previous values.
289 * </p>
290 *
291 * @param comment
292 * The query's comment.
293 * @return This builder for call chaining.
294 *
295 * @see <a
296 * href="http://docs.mongodb.org/manual/reference/operator/meta/comment/">$comment</a>
297 */
298 public QueryBuilder comment(final String comment) {
299 myQueryComment = comment;
300
301 return this;
302 }
303
304 /**
305 * Clears the builder's conditions.
306 */
307 public void reset() {
308 myConditions.clear();
309 myTextQuery = null;
310 myQueryComment = null;
311 }
312
313 /**
314 * Adds a text query to the query builder.
315 * <p>
316 * Only a single {@link #text} condition can be used. Calling multiple
317 * <tt>text(...)</tt> methods overwrites previous values.
318 * </p>
319 *
320 * @param textSearchExpression
321 * The text search expression.
322 * @return This builder for call chaining.
323 *
324 * @see <a
325 * href="http://docs.mongodb.org/manual/tutorial/search-for-text/">Text
326 * Search Expressions</a>
327 */
328 public QueryBuilder text(final String textSearchExpression) {
329 myTextQuery = new DocumentElement(
330 MiscellaneousOperator.TEXT.getToken(), new StringElement(
331 MiscellaneousOperator.SEARCH_MODIFIER,
332 textSearchExpression));
333
334 return this;
335 }
336
337 /**
338 * Adds a text query to the query builder.
339 * <p>
340 * Only a single {@link #text} condition can be used. Calling multiple
341 * <tt>text(...)</tt> methods overwrites previous values.
342 * </p>
343 *
344 * @param textSearchExpression
345 * The text search expression.
346 * @param language
347 * The language of the text search expression.
348 * @return This builder for call chaining.
349 *
350 * @see <a
351 * href="http://docs.mongodb.org/manual/tutorial/search-for-text/">Text
352 * Search Expressions</a>
353 * @see <a
354 * href="http://docs.mongodb.org/manual/reference/command/text/#text-search-languages">Text
355 * Search Languages</a>
356 */
357 public QueryBuilder text(final String textSearchExpression,
358 final String language) {
359 myTextQuery = new DocumentElement(
360 MiscellaneousOperator.TEXT.getToken(), new StringElement(
361 MiscellaneousOperator.SEARCH_MODIFIER,
362 textSearchExpression), new StringElement(
363 MiscellaneousOperator.LANGUAGE_MODIFIER, language));
364
365 return this;
366 }
367
368 /**
369 * Returns a builder for the constraints on a single field.
370 *
371 * @param fieldName
372 * The name of the field to constrain.
373 * @return A {@link ConditionBuilder} for creation of the conditions of the
374 * field.
375 */
376 public ConditionBuilder whereField(final String fieldName) {
377 ConditionBuilder builder = myConditions.get(fieldName);
378 if (builder == null) {
379 builder = new ConditionBuilder(fieldName, this);
380 myConditions.put(fieldName, builder);
381 }
382 return builder;
383 }
384
385 /**
386 * Adds an ad-hoc JavaScript condition to the query.
387 *
388 * @param javaScript
389 * The javaScript condition to add.
390 * @return This builder for call chaining.
391 */
392 public QueryBuilder whereJavaScript(final String javaScript) {
393 myWhere = javaScript;
394
395 return this;
396 }
397 }