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