Coverage Report - com.allanbank.mongodb.builder.QueryBuilder
 
Classes in this File Line Coverage Branch Coverage Complexity
QueryBuilder
100%
83/83
100%
44/44
2.929
 
 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  6
         if (criteria.length <= 0) {
 85  1
             return EmptyDocument.INSTANCE;
 86  
         }
 87  5
         else if (criteria.length == 1) {
 88  2
             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  3
             final Set<String> seen = new HashSet<String>();
 96  3
             DocumentBuilder optimized = BuilderFactory.start();
 97  3
             final DocumentBuilder docBuilder = BuilderFactory.start();
 98  3
             final ArrayBuilder arrayBuilder = docBuilder
 99  
                     .pushArray(LogicalOperator.AND.getToken());
 100  
 
 101  13
             for (final DocumentAssignable criterion : criteria) {
 102  10
                 final Document subQuery = criterion.asDocument();
 103  
                 // Make sure at least 1 element.
 104  10
                 final Iterator<Element> iter = subQuery.iterator();
 105  10
                 if (iter.hasNext()) {
 106  9
                     arrayBuilder.addDocument(subQuery);
 107  
 
 108  17
                     while ((optimized != null) && iter.hasNext()) {
 109  8
                         final Element subQueryElement = iter.next();
 110  8
                         if (seen.add(subQueryElement.getName())) {
 111  6
                             optimized.add(subQueryElement);
 112  
                         }
 113  
                         else {
 114  2
                             optimized = null;
 115  
                         }
 116  8
                     }
 117  
                 }
 118  
             }
 119  
 
 120  3
             if (optimized != null) {
 121  1
                 return optimized.build();
 122  
             }
 123  2
             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  2
         final DocumentBuilder docBuilder = BuilderFactory.start();
 138  2
         final ArrayBuilder arrayBuilder = docBuilder
 139  
                 .pushArray(LogicalOperator.NOR.getToken());
 140  
 
 141  7
         for (final DocumentAssignable criterion : criteria) {
 142  5
             final Document subQuery = criterion.asDocument();
 143  5
             if (subQuery.iterator().hasNext()) {
 144  4
                 arrayBuilder.addDocument(subQuery);
 145  
             }
 146  
         }
 147  
 
 148  2
         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  2
         final DocumentBuilder docBuilder = BuilderFactory.start();
 161  2
         final ArrayBuilder arrayBuilder = docBuilder
 162  
                 .pushArray(LogicalOperator.NOT.getToken());
 163  
 
 164  7
         for (final DocumentAssignable criterion : criteria) {
 165  5
             final Document subQuery = criterion.asDocument();
 166  5
             if (subQuery.iterator().hasNext()) {
 167  4
                 arrayBuilder.addDocument(subQuery);
 168  
             }
 169  
         }
 170  
 
 171  2
         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  4
         if (criteria.length <= 0) {
 184  1
             return EmptyDocument.INSTANCE;
 185  
         }
 186  3
         else if (criteria.length == 1) {
 187  1
             return criteria[0].asDocument();
 188  
         }
 189  
         else {
 190  2
             final DocumentBuilder docBuilder = BuilderFactory.start();
 191  2
             final ArrayBuilder arrayBuilder = docBuilder
 192  
                     .pushArray(LogicalOperator.OR.getToken());
 193  
 
 194  7
             for (final DocumentAssignable criterion : criteria) {
 195  5
                 final Document subQuery = criterion.asDocument();
 196  5
                 if (subQuery.iterator().hasNext()) {
 197  4
                     arrayBuilder.addDocument(subQuery);
 198  
                 }
 199  
             }
 200  
 
 201  2
             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  263
         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  271
     public QueryBuilder() {
 232  271
         myConditions = new LinkedHashMap<String, ConditionBuilder>();
 233  
 
 234  271
         reset();
 235  271
     }
 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  7
         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  58
         final DocumentBuilder builder = BuilderFactory.start();
 257  
 
 258  58
         if (myQueryComment != null) {
 259  1
             builder.add(MiscellaneousOperator.COMMENT.getToken(),
 260  
                     myQueryComment);
 261  
         }
 262  
 
 263  58
         if (myTextQuery != null) {
 264  3
             builder.add(myTextQuery);
 265  
         }
 266  
 
 267  58
         for (final ConditionBuilder condBuilder : myConditions.values()) {
 268  61
             final Element condElement = condBuilder.buildFieldCondition();
 269  
 
 270  61
             if (condElement != null) {
 271  53
                 builder.add(condElement);
 272  
             }
 273  61
         }
 274  
 
 275  58
         if (myWhere != null) {
 276  2
             builder.addJavaScript(MiscellaneousOperator.WHERE.getToken(),
 277  
                     myWhere);
 278  
         }
 279  
 
 280  58
         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  1
         myQueryComment = comment;
 300  
 
 301  1
         return this;
 302  
     }
 303  
 
 304  
     /**
 305  
      * Clears the builder's conditions.
 306  
      */
 307  
     public void reset() {
 308  272
         myConditions.clear();
 309  272
         myTextQuery = null;
 310  272
         myQueryComment = null;
 311  272
     }
 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  2
         myTextQuery = new DocumentElement(
 330  
                 MiscellaneousOperator.TEXT.getToken(), new StringElement(
 331  
                         MiscellaneousOperator.SEARCH_MODIFIER,
 332  
                         textSearchExpression));
 333  
 
 334  2
         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  1
         myTextQuery = new DocumentElement(
 360  
                 MiscellaneousOperator.TEXT.getToken(), new StringElement(
 361  
                         MiscellaneousOperator.SEARCH_MODIFIER,
 362  
                         textSearchExpression), new StringElement(
 363  
                         MiscellaneousOperator.LANGUAGE_MODIFIER, language));
 364  
 
 365  1
         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  278
         ConditionBuilder builder = myConditions.get(fieldName);
 378  278
         if (builder == null) {
 379  275
             builder = new ConditionBuilder(fieldName, this);
 380  275
             myConditions.put(fieldName, builder);
 381  
         }
 382  278
         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  2
         myWhere = javaScript;
 394  
 
 395  2
         return this;
 396  
     }
 397  
 }