1 /*
2 * #%L
3 * Command.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 package com.allanbank.mongodb.client.message;
21
22 import java.io.IOException;
23 import java.util.Iterator;
24
25 import com.allanbank.mongodb.ReadPreference;
26 import com.allanbank.mongodb.bson.Document;
27 import com.allanbank.mongodb.bson.Element;
28 import com.allanbank.mongodb.bson.impl.EmptyDocument;
29 import com.allanbank.mongodb.bson.io.BsonOutputStream;
30 import com.allanbank.mongodb.bson.io.BufferingBsonOutputStream;
31 import com.allanbank.mongodb.bson.io.StringEncoder;
32 import com.allanbank.mongodb.client.Operation;
33 import com.allanbank.mongodb.client.VersionRange;
34 import com.allanbank.mongodb.error.DocumentToLargeException;
35
36 /**
37 * Helper class to make generating command queries easier. Commands are
38 * communicated to the server as {@link Operation#QUERY} messages. We don't use
39 * the Query class as a base class as it adds a lot of weight to the commands.
40 *
41 * @api.no This class is <b>NOT</b> part of the drivers API. This class may be
42 * mutated in incompatible ways between any two releases of the driver.
43 * @copyright 2011-2013, Allanbank Consulting, Inc., All Rights Reserved
44 */
45 public class Command extends AbstractMessage {
46
47 /** The collection to use when issuing commands to the database. */
48 public static final String COMMAND_COLLECTION = "$cmd";
49
50 /** The amount of head room for jumbo documents. */
51 private static final int HEADROOM = 16 * 1024;
52
53 /**
54 * If true then the command document is allowed to slightly exceed the
55 * document size limit.
56 */
57 private boolean myAllowJumbo = false;
58
59 /** The command's document. */
60 private final Document myCommand;
61
62 /** The command's document to use in routing decisions. */
63 private final Document myRoutingDocument;
64
65 /**
66 * Create a new Command.
67 *
68 * @param databaseName
69 * The name of the database.
70 * @param collectionName
71 * The name of the collection the command is using. This should
72 * be the real collection and not {@value #COMMAND_COLLECTION} if
73 * the real collection is known.
74 * @param commandDocument
75 * The command document containing the command and options.
76 */
77 public Command(final String databaseName, final String collectionName,
78 final Document commandDocument) {
79 this(databaseName, collectionName, commandDocument,
80 ReadPreference.PRIMARY);
81 }
82
83 /**
84 * Create a new Command.
85 *
86 * @param databaseName
87 * The name of the database.
88 * @param collectionName
89 * The name of the collection the command is using. This should
90 * be the real collection and not {@value #COMMAND_COLLECTION} if
91 * the real collection is known.
92 * @param commandDocument
93 * The command document containing the command and options.
94 * @param routingDocument
95 * The document that should be used for routing the command.
96 * @param readPreference
97 * The preference for which servers to use to retrieve the
98 * results.
99 * @param requiredServerVersion
100 * The required version of the server to support processing the
101 * message.
102 */
103 public Command(final String databaseName, final String collectionName,
104 final Document commandDocument, final Document routingDocument,
105 final ReadPreference readPreference,
106 final VersionRange requiredServerVersion) {
107 super(databaseName, collectionName, readPreference,
108 requiredServerVersion);
109
110 myCommand = commandDocument;
111 myRoutingDocument = routingDocument;
112 }
113
114 /**
115 * Create a new Command.
116 *
117 * @param databaseName
118 * The name of the database.
119 * @param collectionName
120 * The name of the collection the command is using. This should
121 * be the real collection and not {@value #COMMAND_COLLECTION} if
122 * the real collection is known.
123 * @param commandDocument
124 * The command document containing the command and options.
125 * @param readPreference
126 * The preference for which servers to use to retrieve the
127 * results.
128 */
129 public Command(final String databaseName, final String collectionName,
130 final Document commandDocument, final ReadPreference readPreference) {
131 this(databaseName, collectionName, commandDocument, readPreference,
132 null);
133 }
134
135 /**
136 * Create a new Command.
137 *
138 * @param databaseName
139 * The name of the database.
140 * @param collectionName
141 * The name of the collection the command is using. This should
142 * be the real collection and not {@value #COMMAND_COLLECTION} if
143 * the real collection is known.
144 * @param commandDocument
145 * The command document containing the command and options.
146 * @param readPreference
147 * The preference for which servers to use to retrieve the
148 * results.
149 * @param requiredServerVersion
150 * The required version of the server to support processing the
151 * message.
152 */
153 public Command(final String databaseName, final String collectionName,
154 final Document commandDocument,
155 final ReadPreference readPreference,
156 final VersionRange requiredServerVersion) {
157 this(databaseName, collectionName, commandDocument,
158 EmptyDocument.INSTANCE, readPreference, requiredServerVersion);
159 }
160
161 /**
162 * Determines if the passed object is of this same type as this object and
163 * if so that its fields are equal.
164 *
165 * @param object
166 * The object to compare to.
167 *
168 * @see java.lang.Object#equals(java.lang.Object)
169 */
170 @Override
171 public boolean equals(final Object object) {
172 boolean result = false;
173 if (this == object) {
174 result = true;
175 }
176 else if ((object != null) && (getClass() == object.getClass())) {
177 final Command other = (Command) object;
178
179 result = super.equals(object) && myCommand.equals(other.myCommand);
180 }
181 return result;
182 }
183
184 /**
185 * Returns the command's document.
186 *
187 * @return The command's document.
188 */
189 public Document getCommand() {
190 return myCommand;
191 }
192
193 /**
194 * {@inheritDoc}
195 * <p>
196 * Overridden to return the command name.
197 * </p>
198 */
199 @Override
200 public String getOperationName() {
201 // Return the value for the first element in the document.
202 final Iterator<Element> iter = myCommand.iterator();
203 if (iter.hasNext()) {
204 return iter.next().getName();
205 }
206 // Not expected. Command documents should have atleast one element. Just
207 // return a generic name here.
208 return "command";
209 }
210
211 /**
212 * Returns the routingDocument value.
213 *
214 * @return The routingDocument value.
215 */
216 public Document getRoutingDocument() {
217 return myRoutingDocument;
218 }
219
220 /**
221 * Computes a reasonable hash code.
222 *
223 * @return The hash code value.
224 */
225 @Override
226 public int hashCode() {
227 int result = 1;
228 result = (31 * result) + super.hashCode();
229 result = (31 * result) + myCommand.hashCode();
230 return result;
231 }
232
233 /**
234 * Returns true if the command document is allowed to slightly exceed the
235 * document size limit.
236 *
237 * @return True if the command document is allowed to slightly exceed the
238 * document size limit.
239 */
240 public boolean isAllowJumbo() {
241 return myAllowJumbo;
242 }
243
244 /**
245 * If set to true then the command document is allowed to slightly exceed
246 * the document size limit. This allows us to pack a full size document in a
247 * insert command.
248 *
249 * @param allowJumbo
250 * If true then the command document is allowed to slightly
251 * exceed the document size limit.
252 */
253 public void setAllowJumbo(final boolean allowJumbo) {
254 myAllowJumbo = allowJumbo;
255 }
256
257 /**
258 * {@inheritDoc}
259 * <p>
260 * Overridden to return the size of the {@link Command}.
261 * </p>
262 */
263 @Override
264 public int size() {
265 int size = HEADER_SIZE + 18; // See below.
266 // size += 4; // flags;
267 size += StringEncoder.utf8Size(myDatabaseName);
268 // size += 1; // StringEncoder.utf8Size(".");
269 // size += 4; // StringEncoder.utf8Size(COMMAND_COLLECTION);
270 // size += 1; // \0 on the CString.
271 // size += 4; // numberToSkip
272 // size += 4; // numberToReturn
273 size += myCommand.size();
274
275 return size;
276 }
277
278 /**
279 * {@inheritDoc}
280 * <p>
281 * Overridden to return a human readable form of the command.
282 * </p>
283 */
284 @Override
285 public String toString() {
286 final StringBuilder builder = new StringBuilder();
287 builder.append("Command[");
288 builder.append(getOperationName());
289 builder.append(", db=");
290 builder.append(myDatabaseName);
291 builder.append(", collection=");
292 builder.append(myCollectionName);
293 if (getReadPreference() != null) {
294 builder.append(", readPreference=");
295 builder.append(getReadPreference());
296 }
297 if (getRequiredVersionRange() != null) {
298 builder.append(", requiredVersionRange=");
299 builder.append(getRequiredVersionRange());
300 }
301 builder.append("]: ");
302 builder.append(myCommand);
303
304 return builder.toString();
305 }
306
307 /**
308 * {@inheritDoc}
309 * <p>
310 * Overridden to make sure the command document is not too large.
311 * </p>
312 */
313 @Override
314 public void validateSize(final int maxDocumentSize)
315 throws DocumentToLargeException {
316 final long size = myCommand.size();
317
318 if (isAllowJumbo()) {
319 if ((maxDocumentSize + HEADROOM) < size) {
320 throw new DocumentToLargeException((int) size, maxDocumentSize
321 + HEADROOM, myCommand);
322 }
323 }
324 else if (maxDocumentSize < size) {
325 throw new DocumentToLargeException((int) size, maxDocumentSize,
326 myCommand);
327 }
328 }
329
330 /**
331 * {@inheritDoc}
332 * <p>
333 * Overridden to write the Command as a {@link Operation#QUERY} message.
334 * </p>
335 */
336 @Override
337 public void write(final int messageId, final BsonOutputStream out)
338 throws IOException {
339 final int numberToSkip = 0;
340 final int numberToReturn = -1; // Unlimited
341 final int flags = computeFlags();
342
343 int size = HEADER_SIZE;
344 size += 4; // flags;
345 size += out.sizeOfCString(myDatabaseName, ".", COMMAND_COLLECTION);
346 size += 4; // numberToSkip
347 size += 4; // numberToReturn
348 size += myCommand.size();
349
350 writeHeader(out, messageId, 0, Operation.QUERY, size);
351 out.writeInt(flags);
352 out.writeCString(myDatabaseName, ".", COMMAND_COLLECTION);
353 out.writeInt(numberToSkip);
354 out.writeInt(numberToReturn);
355 out.writeDocument(myCommand);
356 }
357
358 /**
359 * {@inheritDoc}
360 * <p>
361 * Overridden to write the Command as a {@link Operation#QUERY} message.
362 * </p>
363 */
364 @Override
365 public void write(final int messageId, final BufferingBsonOutputStream out)
366 throws IOException {
367 final int numberToSkip = 0;
368 final int numberToReturn = -1; // Unlimited
369 final int flags = computeFlags();
370
371 final long start = writeHeader(out, messageId, 0, Operation.QUERY);
372 out.writeInt(flags);
373 out.writeCString(myDatabaseName, ".", COMMAND_COLLECTION);
374 out.writeInt(numberToSkip);
375 out.writeInt(numberToReturn);
376 out.writeDocument(myCommand);
377 finishHeader(out, start);
378
379 out.flushBuffer();
380 }
381
382 /**
383 * Computes the message flags bit field.
384 *
385 * @return The message flags bit field.
386 */
387 private int computeFlags() {
388 int flags = 0;
389 if (getReadPreference().isSecondaryOk()) {
390 flags += Query.REPLICA_OK_FLAG_BIT;
391 }
392 return flags;
393 }
394 }