Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
QueryBuilder |
|
| 2.9285714285714284;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 <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 | 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 | } |