1 /*
2 * #%L
3 * MongoDbUri.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;
21
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.LinkedHashMap;
25 import java.util.List;
26 import java.util.Locale;
27 import java.util.Map;
28 import java.util.StringTokenizer;
29
30 import com.allanbank.mongodb.bson.element.UuidElement;
31
32 /**
33 * MongoDbUri provides the ability to parse a MongoDB URI into fields.
34 * <p>
35 * The set of possible options is a combination of the <a
36 * href="http://docs.mongodb.org/manual/reference/connection-string/">standard
37 * MongoDB URI options</a> and the complete set of fields supported by the
38 * {@link MongoClientConfiguration} class. The driver uses the Java beans
39 * standard to define the label for each field.
40 * </p>
41 * See the <a
42 * href="http://docs.mongodb.org/manual/reference/connection-string/">Connection
43 * String</a> documentation for information on the standard options. The
44 * following standard options are not supported by the driver:
45 * <dl>
46 * <dt>replicaSet</dt>
47 * <dd>The driver does automatic cluster type discovery so this options is not
48 * needed or used.</dd>
49 * <dt>maxIdleTimeMS</dt>
50 * <dd>The driver does not use a specific wait time. Instead the connection is
51 * considered idle when it experiences a defined number of idle ticks. A tick is
52 * defined by the {@link MongoClientConfiguration#setReadTimeout(int)
53 * readTimeout} and the number of ticks is defined by
54 * {@link MongoClientConfiguration#setMaxIdleTickCount(int) maxIdleTickCount}.</dd>
55 * <dt>waitQueueMultiple</dt>
56 * <dd>The driver does not queue requests waiting for a socket since it is
57 * asynchronous. The closest option would be the
58 * {@link MongoClientConfiguration#setMaxPendingOperationsPerConnection(int)
59 * maxPendingOperationsPerConnection} which can be used to control how
60 * aggressively the driver will apply back pressure.</dd>
61 * <dt>waitQueueTimeoutMS</dt>
62 * <dd>Similar to {@code waitQueueMultiple} this option does not make sense for
63 * an asynchronous driver.</dd>
64 * <dt>uuidRepresentation</dt>
65 * <dd>The UUID representation can only be controlled via construction. See the
66 * {@link UuidElement#UuidElement(String, byte, java.util.UUID)} and
67 * {@link UuidElement#LEGACY_UUID_SUBTTYPE}.</dd>
68 * </dl>
69 * <p>
70 * </p>
71 * <p>
72 * The current set of auto-mapped fields is listed below. See the linked
73 * documentation for details on the fields values.
74 * <ul>
75 * <li>{@link MongoClientConfiguration#setAutoDiscoverServers(boolean)
76 * autoDiscoverServers}</li>
77 * <li>{@link MongoClientConfiguration#setConnectionModel(ConnectionModel)
78 * connectionModel}</li>
79 * <li>{@link MongoClientConfiguration#setConnectTimeout(int) connectTimeout}</li>
80 * <li>{@link MongoClientConfiguration#setLockType(LockType) lockType}</li>
81 * <li>{@link MongoClientConfiguration#setMaxCachedStringEntries(int)
82 * maxCachedStringEntries}</li>
83 * <li>{@link MongoClientConfiguration#setMaxCachedStringLength(int)
84 * maxCachedStringLength}</li>
85 * <li>{@link MongoClientConfiguration#setMaxConnectionCount(int)
86 * maxConnectionCount}</li>
87 * <li>{@link MongoClientConfiguration#setMaxIdleTickCount(int)
88 * maxIdleTickCount}</li>
89 * <li>
90 * {@link MongoClientConfiguration#setMaxPendingOperationsPerConnection(int)
91 * maxPendingOperationsPerConnection}</li>
92 * <li>
93 * {@link MongoClientConfiguration#setMaxSecondaryLag(long) maxSecondaryLag}</li>
94 * <li>
95 * {@link MongoClientConfiguration#setMinConnectionCount(int)
96 * minConnectionCount}</li>
97 * <li>
98 * {@link MongoClientConfiguration#setReadTimeout(int) readTimeout}</li>
99 * <li>
100 * {@link MongoClientConfiguration#setReconnectTimeout(int) reconnectTimeout}</li>
101 * <li>
102 * {@link MongoClientConfiguration#setUsingSoKeepalive(boolean)
103 * usingSoKeepalive}</li>
104 * </ul>
105 * </p>
106 * <p>
107 * Any credentials, default read preference, and default durability will also be
108 * determined via the URI. See the {@link CredentialEditor},
109 * {@link ReadPreferenceEditor}, and {@link DurabilityEditor} for details
110 * </p>
111 *
112 * @see <a href="http://www.mongodb.org/display/DOCS/Connections"> MongoDB
113 * Connections</a>
114 * @api.yes This class is part of the driver's API. Public and protected members
115 * will be deprecated for at least 1 non-bugfix release (version
116 * numbers are <major>.<minor>.<bugfix>) before being
117 * removed or modified.
118 * @copyright 2012-2014, Allanbank Consulting, Inc., All Rights Reserved
119 */
120 public class MongoDbUri {
121 /** The prefix for a MongoDB URI. */
122 public static final String MONGODB_URI_PREFIX = "mongodb://";
123
124 /** The prefix for a MongoDB URI that uses SSL. */
125 public static final String MONGODBS_URI_PREFIX = "mongodbs://";
126
127 /**
128 * Tests if the {@code mongoDbUri} starts with the correct prefix to be a
129 * MongoDB URI.
130 *
131 * @param mongoDbUri
132 * The presumed MongoDB URI.
133 * @return True if the {@code mongoDbUri} starts with
134 * {@value #MONGODB_URI_PREFIX} of {@value #MONGODBS_URI_PREFIX}.
135 */
136 public static boolean isUri(final String mongoDbUri) {
137 final String lower = mongoDbUri.toLowerCase(Locale.US);
138
139 return lower.startsWith(MONGODB_URI_PREFIX)
140 || lower.startsWith(MONGODBS_URI_PREFIX);
141 }
142
143 /** The database contained in the URI. */
144 private final String myDatabase;
145
146 /** The hosts contained in the URI. */
147 private final List<String> myHosts;
148
149 /** The options contained in the URI. */
150 private final String myOptions;
151
152 /** The database contained in the URI. */
153 private final String myOriginalText;
154
155 /** The password contained in the URI. */
156 private final String myPassword;
157
158 /** The user name contained in the URI. */
159 private final String myUserName;
160
161 /**
162 * Set to true if the URL uses the {@value #MONGODBS_URI_PREFIX} prefix.
163 */
164 private final boolean myUseSsl;
165
166 /**
167 * Creates a new MongoDbUri.
168 *
169 * @param mongoDbUri
170 * The configuration for the connection to MongoDB expressed as a
171 * MongoDB URL.
172 * @throws IllegalArgumentException
173 * If the <tt>mongoDbUri</tt> is not a properly formated MongoDB
174 * style URL.
175 *
176 * @see <a href="http://www.mongodb.org/display/DOCS/Connections"> MongoDB
177 * Connections</a>
178 */
179 public MongoDbUri(final String mongoDbUri) throws IllegalArgumentException {
180 myOriginalText = mongoDbUri;
181
182 String remaining;
183 boolean useSsl = false;
184 if (mongoDbUri == null) {
185 throw new IllegalArgumentException(
186 "The MongoDB URI cannot be null.");
187 }
188 else if (mongoDbUri.substring(0, MONGODB_URI_PREFIX.length())
189 .equalsIgnoreCase(MONGODB_URI_PREFIX)) {
190 remaining = mongoDbUri.substring(MONGODB_URI_PREFIX.length());
191 }
192 else if (mongoDbUri.substring(0, MONGODBS_URI_PREFIX.length())
193 .equalsIgnoreCase(MONGODBS_URI_PREFIX)) {
194 remaining = mongoDbUri.substring(MONGODBS_URI_PREFIX.length());
195 useSsl = true;
196 }
197 else {
198 throw new IllegalArgumentException(
199 "The MongoDB URI must start with '" + MONGODB_URI_PREFIX
200 + "'.");
201 }
202
203 String userNamePassword;
204 String hosts;
205
206 int position = remaining.indexOf('@');
207 if (position >= 0) {
208 userNamePassword = remaining.substring(0, position);
209 remaining = remaining.substring(position + 1);
210 }
211 else {
212 userNamePassword = "";
213 }
214
215 // Figure out the DB - if any.
216 position = remaining.indexOf('/');
217 if (position >= 0) {
218 hosts = remaining.substring(0, position);
219 remaining = remaining.substring(position + 1);
220
221 position = remaining.indexOf('?');
222 if (position >= 0) {
223 myDatabase = remaining.substring(0, position);
224 myOptions = remaining.substring(position + 1);
225 }
226 else {
227 myDatabase = remaining;
228 myOptions = "";
229 }
230 }
231 else {
232 myDatabase = "";
233 position = remaining.indexOf('?');
234 if (position >= 0) {
235 hosts = remaining.substring(0, position);
236 myOptions = remaining.substring(position + 1);
237 }
238 else {
239 hosts = remaining;
240 myOptions = "";
241 }
242 }
243
244 // Now the hosts.
245 final StringTokenizer tokenizer = new StringTokenizer(hosts, ",");
246 myHosts = new ArrayList<String>(tokenizer.countTokens());
247 while (tokenizer.hasMoreTokens()) {
248 myHosts.add(tokenizer.nextToken());
249 }
250 if (myHosts.isEmpty()) {
251 throw new IllegalArgumentException(
252 "Must provide at least 1 host to connect to.");
253 }
254
255 // User name and password?
256 if (!userNamePassword.isEmpty()) {
257 position = userNamePassword.indexOf(':');
258 if (position >= 0) {
259 myUserName = userNamePassword.substring(0, position);
260 myPassword = userNamePassword.substring(position + 1);
261 }
262 else {
263 throw new IllegalArgumentException(
264 "The password for the user '" + userNamePassword
265 + "' must be provided.");
266 }
267 }
268 else {
269 myUserName = null;
270 myPassword = null;
271 }
272
273 myUseSsl = useSsl;
274 }
275
276 /**
277 * Returns the database contained in the URI.
278 *
279 * @return The database contained in the URI.
280 */
281 public String getDatabase() {
282 return myDatabase;
283 }
284
285 /**
286 * Returns the hosts contained in the URI.
287 *
288 * @return The hosts contained in the URI.
289 */
290 public List<String> getHosts() {
291 return Collections.unmodifiableList(myHosts);
292 }
293
294 /**
295 * Returns the options contained in the URI. Will never be null bu may be an
296 * empty string.
297 *
298 * @return The options contained in the URI.
299 */
300 public String getOptions() {
301 return myOptions;
302 }
303
304 /**
305 * Returns the options contained in the URI parsed into a map of token
306 * values.
307 *
308 * @return The options contained in the URI.
309 */
310 public Map<String, String> getParsedOptions() {
311 final Map<String, String> parsedOptions = new LinkedHashMap<String, String>();
312 final StringTokenizer tokenizer = new StringTokenizer(getOptions(),
313 "?;&");
314 while (tokenizer.hasMoreTokens()) {
315 String property;
316 String value;
317
318 final String propertyAndValue = tokenizer.nextToken();
319 final int position = propertyAndValue.indexOf('=');
320 if (position >= 0) {
321 property = propertyAndValue.substring(0, position);
322 value = propertyAndValue.substring(position + 1);
323 }
324 else {
325 property = propertyAndValue;
326 value = Boolean.TRUE.toString();
327 }
328
329 // Normalize the names..
330 property = property.toLowerCase(Locale.US);
331
332 // Put the option at the end.
333 parsedOptions.remove(property);
334 parsedOptions.put(property, value);
335 }
336
337 return parsedOptions;
338 }
339
340 /**
341 * Returns the password contained in the URI. May be <code>null</code>.
342 *
343 * @return The password contained in the URI.
344 */
345 public String getPassword() {
346 return myPassword;
347 }
348
349 /**
350 * Returns the user name contained in the URI. May be <code>null</code>.
351 *
352 * @return The user name contained in the URI.
353 */
354 public String getUserName() {
355 return myUserName;
356 }
357
358 /**
359 * Returns the values for a single property. This allows for duplicate
360 * values.
361 *
362 * @param field
363 * The field to return all values for.
364 * @return The options contained in the URI.
365 */
366 public List<String> getValuesFor(final String field) {
367 final List<String> values = new ArrayList<String>();
368 final StringTokenizer tokenizer = new StringTokenizer(getOptions(),
369 "?;&");
370 while (tokenizer.hasMoreTokens()) {
371 String property;
372 String value;
373
374 final String propertyAndValue = tokenizer.nextToken();
375 final int position = propertyAndValue.indexOf('=');
376 if (position >= 0) {
377 property = propertyAndValue.substring(0, position);
378 value = propertyAndValue.substring(position + 1);
379 }
380 else {
381 property = propertyAndValue;
382 value = Boolean.TRUE.toString();
383 }
384
385 if (field.equalsIgnoreCase(property)) {
386 values.add(value);
387 }
388 }
389
390 return values;
391 }
392
393 /**
394 * Returns true if the URL uses the {@value #MONGODBS_URI_PREFIX} prefix.
395 *
396 * @return True if the URL uses the {@value #MONGODBS_URI_PREFIX} prefix.
397 */
398 public boolean isUseSsl() {
399 return myUseSsl;
400 }
401
402 /**
403 * {@inheritDoc}
404 * <p>
405 * Overridden to return the string used to construct the URI.
406 * </p>
407 */
408 @Override
409 public String toString() {
410 return myOriginalText;
411 }
412
413 }