001: /**
002: * Sequoia: Database clustering technology.
003: * Copyright (C) 2005 Emic Networks.
004: * Contact: sequoia@continuent.org
005: *
006: * Licensed under the Apache License, Version 2.0 (the "License");
007: * you may not use this file except in compliance with the License.
008: * You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS,
014: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015: * See the License for the specific language governing permissions and
016: * limitations under the License.
017: *
018: * Initial developer(s): Emmanuel Cecchet.
019: * Contributor(s): ______________________.
020: */package org.continuent.sequoia.driver;
021:
022: import java.net.UnknownHostException;
023: import java.sql.SQLException;
024: import java.util.HashMap;
025: import java.util.Iterator;
026: import java.util.Properties;
027: import java.util.StringTokenizer;
028:
029: import org.continuent.sequoia.driver.connectpolicy.AbstractControllerConnectPolicy;
030: import org.continuent.sequoia.driver.connectpolicy.OrderedConnectPolicy;
031: import org.continuent.sequoia.driver.connectpolicy.PreferredListConnectPolicy;
032: import org.continuent.sequoia.driver.connectpolicy.RandomConnectPolicy;
033: import org.continuent.sequoia.driver.connectpolicy.RoundRobinConnectPolicy;
034: import org.continuent.sequoia.driver.connectpolicy.SingleConnectPolicy;
035:
036: /**
037: * This class defines a Sequoia url with parsed Metadata and so on. The
038: * connection policy is interpreted while reading the URL. We could rename it to
039: * ParsedURL.
040: *
041: * @author <a href="mailto:emmanuel.cecchet@emicnetworks.com">Emmanuel Cecchet
042: * </a>
043: * @version 1.0
044: */
045: public class SequoiaUrl {
046: private Driver driver;
047: private String url;
048: private String databaseName;
049: private ControllerInfo[] controllerList;
050: /** This includes parameters parsed from the url */
051: private final HashMap parameters;
052: private AbstractControllerConnectPolicy controllerConnectPolicy;
053:
054: /**
055: * Default port number to connect to the Sequoia controller
056: */
057: public static final int DEFAULT_CONTROLLER_PORT = 25322;
058:
059: // Debug information
060: private int debugLevel;
061: /** Most verbose level of debug */
062: public static final int DEBUG_LEVEL_DEBUG = 2;
063: /** Informational level of debug */
064: public static final int DEBUG_LEVEL_INFO = 1;
065: /** No debug messages */
066: public static final int DEBUG_LEVEL_OFF = 0;
067:
068: /**
069: * Creates a new <code>SequoiaUrl</code> object, parse it and instantiate
070: * the connection creation policy.
071: *
072: * @param driver driver used to create the connections
073: * @param url the URL to parse
074: * @param props optional filtered connection properties. Must not be null.
075: * @throws SQLException if an error occurs while parsing the url
076: */
077: public SequoiaUrl(Driver driver, String url, Properties props)
078: throws SQLException {
079: this (driver, url, props, null);
080: }
081:
082: private SequoiaUrl(Driver driver, String url, Properties props,
083: AbstractControllerConnectPolicy policy) throws SQLException {
084: this .driver = driver;
085: this .url = url;
086:
087: parameters = new HashMap();
088: // Initialize parameters
089: parameters.putAll(props);
090: // Parse URL now, will override any parameter defined as a property
091: // by the previous line. Fixes SEQUOIA-105.
092: parseUrl();
093:
094: String debugProperty = (String) parameters
095: .get(Driver.DEBUG_PROPERTY);
096: debugLevel = DEBUG_LEVEL_OFF;
097: if (debugProperty != null) {
098: if ("debug".equals(debugProperty))
099: debugLevel = DEBUG_LEVEL_DEBUG;
100: else if ("info".equals(debugProperty))
101: debugLevel = DEBUG_LEVEL_INFO;
102: }
103:
104: controllerConnectPolicy = (policy != null) ? policy
105: : createConnectionPolicy();
106: }
107:
108: /**
109: * Returns the debugLevel value.
110: *
111: * @return Returns the debugLevel.
112: */
113: public int getDebugLevel() {
114: return debugLevel;
115: }
116:
117: /**
118: * Returns true if debugging is set to debug level 'debug'
119: *
120: * @return true if debugging is enabled
121: */
122: public boolean isDebugEnabled() {
123: return debugLevel == DEBUG_LEVEL_DEBUG;
124: }
125:
126: /**
127: * Returns true if debug level is 'info' or greater
128: *
129: * @return true if debug level is 'info' or greater
130: */
131: public boolean isInfoEnabled() {
132: return debugLevel >= DEBUG_LEVEL_INFO;
133: }
134:
135: /**
136: * Returns the controllerConnectPolicy value.
137: *
138: * @return Returns the controllerConnectPolicy.
139: */
140: public AbstractControllerConnectPolicy getControllerConnectPolicy() {
141: return controllerConnectPolicy;
142: }
143:
144: /**
145: * Returns the controllerList value.
146: *
147: * @return Returns the controllerList.
148: */
149: public ControllerInfo[] getControllerList() {
150: return controllerList;
151: }
152:
153: /**
154: * Returns the database name.
155: *
156: * @return Returns the database name.
157: */
158: public String getDatabaseName() {
159: return databaseName;
160: }
161:
162: /**
163: * Returns the URL parameters/value in a HashMap (warning this is not a
164: * clone). Parameters come from both the parsed URL and the properties
165: * argument if any.
166: * <p>
167: * The HashMap is 'parameter name'=>'value'
168: *
169: * @return Returns the parameters and their value in a Hasmap.
170: */
171: public HashMap getParameters() {
172: return parameters;
173: }
174:
175: /**
176: * Returns the url value.
177: *
178: * @return Returns the url.
179: */
180: public String getUrl() {
181: return url;
182: }
183:
184: /**
185: * Sets the url value.
186: *
187: * @param url The url to set.
188: */
189: public void setUrl(String url) {
190: this .url = url;
191: }
192:
193: //
194: // Private methods (mainly parsing)
195: //
196:
197: /**
198: * Create the corresponding controller connect policy according to what is
199: * found in the URL. If no policy was specified then a
200: * <code>RandomConnectPolicy</code> is returned.
201: *
202: * @return an <code>AbstractControllerConnectPolicy</code>
203: */
204: private AbstractControllerConnectPolicy createConnectionPolicy() {
205: int pingDelayInMs = Driver.DEFAULT_PING_DELAY_IN_MS;
206: String pingDelayStr = (String) parameters
207: .get(Driver.PING_DELAY_IN_MS_PROPERTY);
208: if (pingDelayStr != null)
209: pingDelayInMs = Integer.parseInt(pingDelayStr);
210:
211: int controllerTimeoutInMs = Driver.DEFAULT_CONTROLLER_TIMEOUT_IN_MS;
212: String controllerTimeoutStr = (String) parameters
213: .get(Driver.CONTROLLER_TIMEOUT_IN_MS_PROPERTY);
214: if (controllerTimeoutStr != null)
215: controllerTimeoutInMs = Integer
216: .parseInt(controllerTimeoutStr);
217:
218: if (controllerList.length == 1)
219: return new SingleConnectPolicy(controllerList,
220: pingDelayInMs, controllerTimeoutInMs, debugLevel);
221:
222: String policy = (String) parameters
223: .get(Driver.PREFERRED_CONTROLLER_PROPERTY);
224:
225: // preferredController: defines the strategy to use to choose a preferred
226: // controller to connect to
227: // - jdbc:sequoia://node1,node2,node3/myDB?preferredController=roundRobin
228: // round robin starting with first node in URL
229: // This is the default policy.
230: if ((policy == null) || policy.equals("roundRobin"))
231: return new RoundRobinConnectPolicy(controllerList,
232: pingDelayInMs, controllerTimeoutInMs, debugLevel);
233:
234: // - jdbc:sequoia://node1,node2,node3/myDB?preferredController=ordered
235: // Always connect to node1, and if not available then try to node2 and
236: // finally if none are available try node3.
237: if (policy.equals("ordered"))
238: return new OrderedConnectPolicy(controllerList,
239: pingDelayInMs, controllerTimeoutInMs, debugLevel);
240:
241: // - jdbc:sequoia://node1,node2,node3/myDB?preferredController=random
242: // Pick a node randomly amongst active controllers.
243: if (policy.equals("random"))
244: return new RandomConnectPolicy(controllerList,
245: pingDelayInMs, controllerTimeoutInMs, debugLevel);
246:
247: // - jdbc:sequoia://node1,node2,node3/myDB?preferredController=node2,node3
248: // same as above but round-robin (or random?) between 2 and 3
249: return new PreferredListConnectPolicy(controllerList, policy,
250: pingDelayInMs, controllerTimeoutInMs, debugLevel);
251: }
252:
253: /**
254: * Checks for URL correctness and extract database name, controller list and
255: * parameters into the map "this.parameters".
256: *
257: * @exception SQLException if an error occurs.
258: */
259: private void parseUrl() throws SQLException {
260: // Find the hostname and check for URL correctness
261: if (url == null) {
262: throw new IllegalArgumentException(
263: "Illegal null URL in parseURL(String) method");
264: }
265:
266: if (!url.toLowerCase().startsWith(driver.sequoiaUrlHeader))
267: throw new SQLException("Malformed header from URL '" + url
268: + "' (expected '" + driver.sequoiaUrlHeader + "')");
269: else {
270: // Get the controllers list
271: int nextSlash = url.indexOf('/',
272: driver.sequoiaUrlHeaderLength);
273: if (nextSlash == -1)
274: // Missing '/' between hostname and database name.
275: throw new SQLException("Malformed URL '" + url
276: + "' (expected '" + driver.sequoiaUrlHeader
277: + "<hostname>/<database>')");
278:
279: // Found end of database name
280: int questionMark = url.indexOf('?', nextSlash);
281: questionMark = (questionMark == -1) ? url.indexOf(';',
282: nextSlash) : questionMark;
283:
284: String controllerURLs = url.substring(
285: driver.sequoiaUrlHeaderLength, nextSlash);
286: // Check the validity of each controller in the list
287: // empty tokens (when successive delims) are ignored
288: StringTokenizer controllers = new StringTokenizer(
289: controllerURLs, ",", false);
290: int tokenNumber = controllers.countTokens();
291: if (tokenNumber == 0) {
292: throw new SQLException("Empty controller name in '"
293: + controllerURLs + "' in URL '" + url + "'");
294: }
295: controllerList = new ControllerInfo[tokenNumber];
296: int i = 0;
297: String token;
298: // TODO: the following code does not recognize the following buggy urls:
299: // jdbc:sequoia://,localhost:/tpcw or jdbc:sequoia://host1,,host2:/tpcw
300: while (controllers.hasMoreTokens()) {
301: token = controllers.nextToken().trim();
302: if (token.equals("")) // whitespace tokens
303: {
304: throw new SQLException("Empty controller name in '"
305: + controllerURLs + "' in URL '" + url + "'");
306: }
307: controllerList[i] = parseController(token);
308: i++;
309: }
310:
311: // Check database name validity
312: databaseName = (questionMark == -1) ? url.substring(
313: nextSlash + 1, url.length()) : url.substring(
314: nextSlash + 1, questionMark);
315: Character c = validDatabaseName(databaseName);
316: if (c != null)
317: throw new SQLException(
318: "Unable to validate database name (unacceptable character '"
319: + c + "' in database '" + databaseName
320: + "' from URL '" + url + "')");
321:
322: // Get the parameters from the url
323: parameters.putAll(parseUrlParams(url));
324: }
325: }
326:
327: /**
328: * Parse the given URL and returns the parameters in a HashMap containing
329: * ParamaterName=>Value.
330: *
331: * @param urlString the URL to parse
332: * @return a Hashmap of param name=>value possibly empty
333: * @throws SQLException if an error occurs
334: */
335: private HashMap parseUrlParams(String urlString)
336: throws SQLException {
337: HashMap props = parseUrlParams(urlString, '?', "&", "=");
338: if (props == null)
339: props = parseUrlParams(urlString, ';', ";", "=");
340: if (props == null)
341: props = new HashMap();
342:
343: return props;
344: }
345:
346: /**
347: * Parse the given URL looking for parameters starting after the beginMarker,
348: * using parameterSeparator as the separator between parameters and equal as
349: * the delimiter between a parameter and its value.
350: *
351: * @param urlString the URL to parse
352: * @param beginMarker delimiter for beginning of parameters
353: * @param parameterSeparator delimiter between parameters
354: * @param equal delimiter between parameter and its value
355: * @return HashMap of ParameterName=>Value
356: * @throws SQLException if an error occurs
357: */
358: private HashMap parseUrlParams(String urlString, char beginMarker,
359: String parameterSeparator, String equal)
360: throws SQLException {
361: int questionMark = urlString.indexOf(beginMarker);
362: if (questionMark == -1)
363: return null;
364: else {
365: HashMap props = new HashMap();
366: String params = urlString.substring(questionMark + 1);
367: StringTokenizer st1 = new StringTokenizer(params,
368: parameterSeparator);
369: while (st1.hasMoreTokens()) {
370: String param = st1.nextToken();
371: StringTokenizer st2 = new StringTokenizer(param, equal);
372: if (st2.hasMoreTokens()) {
373: try {
374: String paramName = st2.nextToken();
375: String paramValue = (st2.hasMoreTokens()) ? st2
376: .nextToken() : "";
377: props.put(paramName, paramValue);
378: } catch (Exception e) // TODOC: what are we supposed to catch here?
379: {
380: throw new SQLException(
381: "Invalid parameter in URL: "
382: + urlString);
383: }
384: }
385: }
386: return props;
387: }
388: }
389:
390: /**
391: * Checks the validity of the hostname, port number and controller name given
392: * in the URL and build the full URL used to lookup a controller.
393: *
394: * @param controller information regarding a controller.
395: * @return a <code>ControllerInfo</code> object
396: * @exception SQLException if an error occurs.
397: */
398: public static ControllerInfo parseController(String controller)
399: throws SQLException {
400: String hostname = null;
401: int hostport = -1;
402:
403: // Check controller syntax
404: StringTokenizer controllerURL = new StringTokenizer(controller,
405: ":", true);
406:
407: // Get hostname
408: hostname = controllerURL.nextToken();
409: Character c = validHostname(hostname);
410: if (c != null)
411: throw new SQLException(
412: "Unable to validate hostname (unacceptable character '"
413: + c + "' in hostname '" + hostname
414: + "' from the URL part '" + controller
415: + "')");
416:
417: if (!controllerURL.hasMoreTokens())
418: hostport = DEFAULT_CONTROLLER_PORT;
419: else {
420: controllerURL.nextToken(); // should be ':'
421: if (!controllerURL.hasMoreTokens())
422: hostport = DEFAULT_CONTROLLER_PORT;
423: else { // Get the port number
424: String port = controllerURL.nextToken();
425: if (controllerURL.hasMoreTokens())
426: throw new SQLException(
427: "Invalid controller definition with more than one semicolon in URL part '"
428: + controller + "'");
429:
430: // Check the port number validity
431: try {
432: hostport = Integer.parseInt(port);
433: } catch (NumberFormatException ne) {
434: throw new SQLException(
435: "Unable to validate port number (unacceptable port number '"
436: + port + "' in this URL part '"
437: + controller + "')");
438: }
439: }
440: }
441: try {
442: return new ControllerInfo(hostname, hostport);
443: } catch (UnknownHostException e) {
444: SQLException err = new SQLException(
445: "Unable to determine address for host " + hostname);
446: err.initCause(e);
447: throw err;
448: }
449: }
450:
451: /**
452: * Checks that the given name contains acceptable characters for a hostname
453: * name ([0-9][A-Z][a-z][["-_."]).
454: *
455: * @param hostname name to check (caller must check that it is not
456: * <code>null</code>).
457: * @return <code>null</code> if the hostname is acceptable, else the
458: * character that causes the fault.
459: */
460: private static Character validHostname(String hostname) {
461: char[] name = hostname.toCharArray();
462: int size = hostname.length();
463: char c;
464: // boolean lastCharWasPoint = false; // used to avoid '..' in hostname
465: char lastChar = ' ';
466:
467: for (int i = 0; i < size; i++) {
468: c = name[i];
469:
470: if (c == '.' || c == '-') {
471: if (lastChar == '.' || lastChar == '-'
472: || (i == size - 1) || (i == 0)) {
473: // . or - cannot be the first or the last char of hostname
474: // hostname cannot contain '..' or '.-' or '-.' or '--'
475: return new Character(c);
476: }
477: } else {
478: if (((c < '0') || (c > 'z') || ((c > '9') && (c < 'A'))
479: || ((c > 'Z') && (c < '_')) || (c == '`'))) {
480: return new Character(c);
481: }
482: }
483: lastChar = c;
484: }
485: return null;
486: }
487:
488: /**
489: * Checks that the given name contains acceptable characters for a database
490: * name ([0-9][A-Z][a-z]["-_"]).
491: *
492: * @param databaseName name to check (caller must check that it is not
493: * <code>null</code>).
494: * @return <code>null</code> if the name is acceptable, else the character
495: * that causes the fault.
496: */
497: private static Character validDatabaseName(String databaseName) {
498: char[] name = databaseName.toCharArray();
499: int size = databaseName.length();
500: char c;
501:
502: for (int i = 0; i < size; i++) {
503: c = name[i];
504: if ((c < '-') || (c > 'z') || (c == '/') || (c == '.')
505: || (c == '`') || ((c > '9') && (c < 'A'))
506: || ((c > 'Z') && (c < '_')))
507: return new Character(c);
508: }
509: return null;
510: }
511:
512: /**
513: * @see java.lang.Object#equals(java.lang.Object)
514: */
515: public boolean equals(Object other) {
516: if (!(other instanceof SequoiaUrl))
517: return false;
518: SequoiaUrl castedOther = (SequoiaUrl) other;
519: return (url.equals(castedOther.url) && parameters
520: .equals(castedOther.parameters));
521: }
522:
523: /**
524: * @see java.lang.Object#hashCode()
525: */
526: public int hashCode() {
527: return toString().hashCode();
528: }
529:
530: /**
531: * @see java.lang.Object#toString()
532: */
533: public String toString() {
534: return (url + parameters);
535: }
536:
537: /**
538: * This clone has the following "differences" with the original:
539: * <ul>
540: * <li>not persistent
541: * <li>but sharing the same controller policy anyway
542: * <ul>
543: */
544: public SequoiaUrl getTemporaryCloneForReconnection(String user,
545: String password) throws SQLException {
546: Properties properties = new Properties();
547: for (Iterator iter = getParameters().keySet().iterator(); iter
548: .hasNext();) {
549: String key = (String) iter.next();
550: properties.setProperty(key, (String) getParameters().get(
551: key));
552: }
553: // The SequoiaUrl does not carry the login/password info so we have to
554: // re-create these properties to reconnect
555: properties.put(Driver.USER_PROPERTY, user);
556: properties.put(Driver.PASSWORD_PROPERTY, password);
557:
558: // Reuse the same controller policy so we do not fork new pingers with
559: // different controller states
560: SequoiaUrl tempUrl = new SequoiaUrl(this .driver, getUrl(),
561: properties, this .controllerConnectPolicy);
562:
563: // Fixes SEQUOIA-568
564: tempUrl.getParameters().put(
565: Driver.PERSISTENT_CONNECTION_PROPERTY, "false");
566:
567: return tempUrl;
568: }
569: }
|