View Javadoc
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 &lt;major&gt;.&lt;minor&gt;.&lt;bugfix&gt;) 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 }