Main Page | Packages | Class Hierarchy | Alphabetical List | Class List | File List | Class Members | Related Pages

Driver.java

00001 /**
00002  * C-JDBC: Clustered JDBC.
00003  * Copyright (C) 2002-2005 French National Institute For Research In Computer
00004  * Science And Control (INRIA).
00005  * Contact: c-jdbc@objectweb.org
00006  * 
00007  * This library is free software; you can redistribute it and/or modify it
00008  * under the terms of the GNU Lesser General Public License as published by the
00009  * Free Software Foundation; either version 2.1 of the License, or any later
00010  * version.
00011  * 
00012  * This library is distributed in the hope that it will be useful, but WITHOUT
00013  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
00014  * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
00015  * for more details.
00016  * 
00017  * You should have received a copy of the GNU Lesser General Public License
00018  * along with this library; if not, write to the Free Software Foundation,
00019  * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
00020  *
00021  * Initial developer(s): Emmanuel Cecchet.
00022  * Contributor(s): Julie Marguerite, Mathieu Peltier, Marek Prochazka, Sara
00023  * Bouchenak, Jaco Swart.
00024  */
00025 
00026 package org.objectweb.cjdbc.driver;
00027 
00028 import java.io.IOException;
00029 import java.net.Socket;
00030 import java.sql.DriverPropertyInfo;
00031 import java.sql.SQLException;
00032 import java.util.ArrayList;
00033 import java.util.Enumeration;
00034 import java.util.HashMap;
00035 import java.util.Hashtable;
00036 import java.util.Properties;
00037 import java.util.Random;
00038 import java.util.StringTokenizer;
00039 
00040 import javax.net.SocketFactory;
00041 
00042 import org.objectweb.cjdbc.common.net.SSLConfiguration;
00043 import org.objectweb.cjdbc.common.net.SocketFactoryFactory;
00044 import org.objectweb.cjdbc.common.sql.filters.AbstractBlobFilter;
00045 import org.objectweb.cjdbc.common.stream.CJDBCInputStream;
00046 import org.objectweb.cjdbc.common.stream.CJDBCOutputStream;
00047 import org.objectweb.cjdbc.common.util.Constants;
00048 import org.objectweb.cjdbc.controller.core.ControllerConstants;
00049 import org.objectweb.cjdbc.driver.protocol.Commands;
00050 
00051 /**
00052  * C-JDBC Driver for client side. This driver is a generic driver that is
00053  * designed to replace any specific JDBC driver that could be used by a client.
00054  * The client only has to know the node where the C-JDBC controller is running
00055  * and the database he wants to access (the RDBMS could be PostgreSQL, Oracle,
00056  * DB2, Sybase, MySQL or whatever, we only need the name of the database and the
00057  * C-JDBC controller will be responsible for finding the RDBMs hosting this
00058  * database).
00059  * <p>
00060  * The C-JDBC driver can be loaded from the client with:
00061  * <code>Class.forName("org.objectweb.cjdbc.driver.Driver");</code>
00062  * <p>
00063  * The URL expected for the use with C-JDBC is:
00064  * <code>jdbc:cjdbc://host1:port1,host2:port2/database</code>.
00065  * <p>
00066  * At least one host must be specified. If several hosts are given, one is
00067  * picked up randomly from the list. If the currently selected controller fails,
00068  * another one is automatically picked up from the list.
00069  * <p>
00070  * Default port number is 25322 if omitted.
00071  * <p>
00072  * Those 2 examples are equivalent:
00073  * 
00074  * <pre>
00075  * DriverManager.getConnection(&quot;jdbc:cjdbc://localhost:/tpcw&quot;);
00076  * DriverManager.getConnection(&quot;jdbc:cjdbc://localhost:25322/tpcw&quot;);
00077  * </pre>
00078  * 
00079  * <p>
00080  * Examples using 2 controllers for fault tolerance:
00081  * 
00082  * <pre>
00083  * DriverManager
00084  *     .getConnection(&quot;jdbc:cjdbc://cluster1.objectweb.org:25322,cluster2.objectweb.org:25322/tpcw&quot;);
00085  * DriverManager
00086  *     .getConnection(&quot;jdbc:cjdbc://localhost:25322,remote.objectweb.org:25322/tpcw&quot;);
00087  * DriverManager
00088  *     .getConnection(&quot;jdbc:cjdbc://smpnode.com:25322,smpnode.com:1098/tpcw&quot;);
00089  * </pre>
00090  * 
00091  * <p>
00092  * This code has been inspired from the PostgreSQL JDBC driver by Peter T. Mount
00093  * <peter@retep.org.uk>and the MM MySQL JDBC Drivers from Mark Matthews
00094  * <mmatthew@worldserver.com>.
00095  * 
00096  * @author <a href="mailto:Emmanuel.Cecchet@inria.fr">Emmanuel Cecchet </a>
00097  * @author <a href="mailto:Julie.Marguerite@inria.fr">Julie Marguerite </a>
00098  * @author <a href="mailto:Mathieu.Peltier@inrialpes.fr">Mathieu Peltier </a>
00099  * @author <a href="mailto:Marek.Prochazka@inrialpes.fr">Marek Prochazka </a>
00100  * @author <a href="mailto:Nicolas.Modrzyk@inrialpes.fr">Nicolas Modrzyk </a>
00101  * @author <a href="mailto:jaco.swart@iblocks.co.uk">Jaco Swart </a>
00102  * @version 1.0
00103  */
00104 
00105 public class Driver implements java.sql.Driver
00106 {
00107   /** Driver major version. */
00108   public static final int       MAJOR_VERSION                            = Constants
00109                                                                              .getMajorVersion();
00110 
00111   /** Driver minor version. */
00112   public static final int       MINOR_VERSION                            = Constants
00113                                                                              .getMinorVersion();
00114 
00115   /** C-JDBC driver property name. */
00116   private static final String   HOST_PROPERTY                            = "HOST";
00117   private static final String   PORT_PROPERTY                            = "PORT";
00118   private static final String   CONTROLLER_PROPERTY                      = "CONTROLLER";
00119   protected static final String DATABASE_PROPERTY                        = "DATABASE";
00120   protected static final String USER_PROPERTY                            = "user";
00121   protected static final String PASSWORD_PROPERTY                        = "password";
00122   protected static final String PARAMETER_PROPERTY                       = "parameter";
00123   protected static final String BOOLEAN_TRUE_PROPERTY                    = "booleanTrue";
00124   protected static final String BOOLEAN_FALSE_PROPERTY                   = "booleanFalse";
00125   protected static final String ESCAPE_BACKSLASH_PROPERTY                = "escapeBackslash";
00126   protected static final String ESCAPE_SINGLE_QUOTE_PROPERTY             = "escapeSingleQuote";
00127   protected static final String ESCAPE_CHARACTER_PROPERTY                = "escapeCharacter";
00128   protected static final String DRIVER_PROCESSED_PROPERTY                = "driverProcessed";
00129 
00130   /** C-JDBC driver property description. */
00131   private static final String   HOST_PROPERTY_DESCRIPTION                = "Hostname of C-JDBC controller";
00132   private static final String   PORT_PROPERTY_DESCRIPTION                = "Port number of C-JDBC controller";
00133   private static final String   DATABASE_PROPERTY_DESCRIPTION            = "Database name";
00134   private static final String   USER_PROPERTY_DESCRIPTION                = "Username to authenticate as";
00135   private static final String   PASSWORD_PROPERTY_DESCRIPTION            = "Password to use for authentication";
00136   private static final String   BOOLEAN_TRUE_PROPERTY_DESCRIPTION        = "Use this value for the 'true' value when using PreparedStatement.setBoolean method";
00137   private static final String   BOOLEAN_FALSE_PROPERTY_DESCRIPTION       = "Use this value for the 'false' value when using PreparedStatement.setBoolean method";
00138   private static final String   ESCAPE_BACKSLASH_PROPERTY_DESCRIPTION    = "Set this to true to escape backslashes when performing escape processing of PreparedStatements";
00139   private static final String   ESCAPE_SINGLE_QUOTE_PROPERTY_DESCRIPTION = "Set this to true to escape single quotes (') when performing escape processing of PreparedStatements";
00140   private static final String   ESCAPE_CHARACTER_PROPERTY_DESCRIPTION    = "Use this character to prepend and append to the values when performing escape processing of PreparedStatements";
00141   private static final String   DRIVER_PROCESSED_PROPERTY_DESCRIPTION    = "Set this to false to let queries be passed and prepared for each individual backend";
00142 
00143   /** C-JDBC URL header. */
00144   private static final String   CJDBC_URL_HEADER                         = "jdbc:cjdbc://";
00145 
00146   /** C-JDBC URL header length. */
00147   private static final int      CJDBC_URL_HEADER_LENGTH                  = CJDBC_URL_HEADER
00148                                                                              .length();
00149 
00150   /**
00151    * Cache of parsed URL (<code>ControllerInfo</code> objects defined at the
00152    * end of this class) used to connect to the controller.
00153    */
00154   private HashMap               controllerCache                          = new HashMap();
00155 
00156   /** Cache of database names and matching URLs. */
00157   private HashMap               dbNameCache                              = new HashMap();
00158 
00159   /** List of connections that are ready to be closed. */
00160   protected ArrayList           pendingConnectionClosing                 = new ArrayList();
00161   protected boolean             connectionClosingThreadisAlive           = false;
00162 
00163   /** Controller. */
00164   private String                currentControllerURL                     = null;
00165   private ArrayList             controllerConfig                         = null;
00166   private ControllerInfo        currentControllerConfig                  = null;
00167   private String                currentDatabase                          = null;
00168   private Random                random;
00169   private int                   controllerConfigArraySize;
00170   private int                   connectionRequestSinceControllerFailure  = 0;
00171   // Retry to reconnect to a controller after RETRY_CONTROLLER_AFTER_FAILURE
00172   // connection requests have been treated since the failure.
00173   private static final int      RETRY_CONTROLLER_AFTER_FAILURE           = 10;
00174 
00175   // The static initializer registers ourselves with the DriverManager
00176   // and try to bind the C-JDBC Controller
00177   static
00178   {
00179     // Register with the DriverManager (see JDBC API Tutorial and Reference,
00180     // Second Edition p. 941)
00181     try
00182     {
00183       java.sql.DriverManager.registerDriver(new Driver());
00184     }
00185     catch (SQLException e)
00186     {
00187       throw new RuntimeException("Unable to register C-JDBC driver");
00188     }
00189   }
00190 
00191   /**
00192    * Creates a new <code>Driver</code> and register it with
00193    * <code>DriverManager</code>.
00194    */
00195   public Driver()
00196   {
00197     // Required for Class.forName().newInstance()
00198     random = new Random(System.currentTimeMillis());
00199   }
00200 
00201   /**
00202    * Asks the C-JDBC controller if the requested database can be accessed with
00203    * the provided user name and password. If the C-JDBC controller can't access
00204    * the requested database, an <code>SQLException</code> is thrown, else a
00205    * "fake" <code>Connection</code> is returned to the user so that he or she
00206    * can create <code>Statements</code>.
00207    * 
00208    * @param url the URL of the C-JDBC controller to which to connect.
00209    * @param info a list of arbitrary string tag/value pairs as connection
00210    *          arguments (usually at least a "user" and "password").
00211    * @return a <code>Connection</code> object that represents a connection to
00212    *         the database through the C-JDBC Controller.
00213    * @exception SQLException if an error occurs.
00214    */
00215   public java.sql.Connection connect(String url, Properties info)
00216       throws SQLException
00217   {
00218     if (url == null)
00219       throw new SQLException("Invalid null URL in connect");
00220 
00221     /**
00222      * We cannot raise a SQLException as the driver manager tries to connect to
00223      * all registered drivers. So if the CJDBC_URL_HEADER is not found we should
00224      * probably pass... and return <code>null</code>
00225      */
00226     if (!url.toLowerCase().startsWith(CJDBC_URL_HEADER))
00227       return null;
00228 
00229     String user = null;
00230     if (info != null)
00231       user = info.getProperty(USER_PROPERTY);
00232     else
00233     {
00234       info = new Properties();
00235     }
00236 
00237     // Parse url to get parameters
00238     Hashtable props = this.parseUrlParams(url);
00239     if (props != null)
00240       info.putAll(props);
00241 
00242     if (user == null || user.equals(""))
00243     {
00244       if ((user = info.getProperty(USER_PROPERTY)) == null)
00245         throw new SQLException("Invalid null user name in connect");
00246     }
00247 
00248     String password = info.getProperty(PASSWORD_PROPERTY);
00249     if (password == null)
00250     {
00251       password = "";
00252     }
00253 
00254     // SSL enabled ?
00255     SSLConfiguration ssl = null;
00256     if ("true".equals(System.getProperty("cjdbc.ssl.enabled")))
00257     {
00258       ssl = SSLConfiguration.getDefaultConfig();
00259     }
00260 
00261     // Check if there is a connection that is about to be closed that could
00262     // be reused. We take the bet that if a connection has been released by
00263     // a client, in the general case, it will reuse the same connection.
00264     // As we need to keep the work in the synchronized block as minimal as
00265     // possible, we have to extract the string comparison (url,name,password)
00266     // from the sync block. This way, we cannot just read/compare/take the
00267     // connection without synchronizing the whole thing. A solution is to
00268     // systematically extract the first available connection in the sync block,
00269     // and do the checkings outside the block. If we fail, we re-sync to put
00270     // the connection back but in practice it is almost always a success and
00271     // we don't really care to pay this extra cost once in a while.
00272     try
00273     {
00274       Connection c;
00275       synchronized (pendingConnectionClosing)
00276       {
00277         // Take the last one to prevent shifting all elements
00278         c = (Connection) pendingConnectionClosing
00279             .remove(pendingConnectionClosing.size() - 1);
00280       }
00281       if (url.equals(c.getURL()) && user.equals(c.getUserName())
00282           && password.equals(c.getPassword()))
00283       { // Great! Take this one.
00284         c.isClosed = false;
00285         return setParametersOnConnection(info, c);
00286       }
00287       else
00288         // Put this connection back, it is not good for us
00289         synchronized (pendingConnectionClosing)
00290         {
00291           pendingConnectionClosing.add(c);
00292         }
00293     }
00294     catch (IndexOutOfBoundsException ignore)
00295     {
00296       // No connection available
00297     }
00298 
00299     int retry = 2;
00300     int picked;
00301     while (retry > 0)
00302     {
00303       // Check if the last url used was the same, if yes we re-use the
00304       // same controller and connection point.
00305       synchronized (this)
00306       {
00307         if (!url.equals(currentControllerURL)
00308             || (connectionRequestSinceControllerFailure >= RETRY_CONTROLLER_AFTER_FAILURE)
00309             || ((controllerConfig != null) && (controllerConfig.size() == 0)))
00310         { // Too bad, all caches missed
00311           ControllerInfo[] controllerConfigArray = (ControllerInfo[]) controllerCache
00312               .get(url);
00313           if (controllerConfigArray == null) // Not in the cache
00314           {
00315             parseURL(url);
00316             controllerConfigArray = (ControllerInfo[]) controllerCache.get(url);
00317           }
00318           controllerConfig = new ArrayList();
00319           controllerConfigArraySize = controllerConfigArray.length;
00320           for (int i = 0; i < controllerConfigArraySize; i++)
00321             controllerConfig.add(controllerConfigArray[i]);
00322 
00323           currentControllerURL = url;
00324           currentDatabase = (String) dbNameCache.get(url);
00325           if (currentDatabase == null)
00326             throw new SQLException("Database name cache failure");
00327 
00328           // Give all controllers a chance
00329           retry = controllerConfig.size();
00330           connectionRequestSinceControllerFailure = 0;
00331         }
00332       }
00333 
00334       int size = controllerConfig.size();
00335       if (size < controllerConfigArraySize)
00336         connectionRequestSinceControllerFailure++;
00337 
00338       // Pick one controller randomly among the controllers that have not been
00339       // tried yet.
00340       picked = random.nextInt(size);
00341       currentControllerConfig = (ControllerInfo) controllerConfig.get(picked);
00342 
00343       boolean sentVdbName = false;
00344       boolean sentUserInfo = false;
00345       try
00346       {
00347         // Connect to the controller
00348         Socket socket = null;
00349         if (ssl == null)
00350         {
00351           // no ssl - we use ordinary socket
00352           socket = new Socket(currentControllerConfig.getHostname(),
00353               currentControllerConfig.getPort());
00354         }
00355         else
00356         {
00357           SocketFactory sslFact = SocketFactoryFactory.createFactory(ssl);
00358           socket = sslFact.createSocket(currentControllerConfig.getHostname(),
00359               currentControllerConfig.getPort());
00360         }
00361 
00362         // Disable Nagle algorithm else small messages are not sent
00363         // (at least under Linux) even if we flush the output stream.
00364         socket.setTcpNoDelay(true);
00365 
00366         CJDBCOutputStream out = new CJDBCOutputStream(socket);
00367         // Send protocol version and database name
00368         out.writeInt(Commands.ProtocolVersion);
00369         out.writeUTF(currentDatabase);
00370         out.flush();
00371         sentVdbName = true;
00372 
00373         // Send user information
00374         out.writeUTF(user);
00375         out.writeUTF(password);
00376         out.flush();
00377         sentUserInfo = true;
00378 
00379         CJDBCInputStream in;
00380         boolean needSkeleton;
00381         AbstractBlobFilter filter = null;
00382         try
00383         {
00384           // Create input stream only here else it will block
00385           in = new CJDBCInputStream(socket);
00386 
00387           Object response = in.readObject();
00388           if (response instanceof Boolean)
00389           {
00390             needSkeleton = ((Boolean) response).booleanValue();
00391             String sfilter = in.readUTF();
00392             filter = AbstractBlobFilter.getBlobFilterInstance(sfilter);
00393           }
00394           else if (response instanceof SQLException)
00395             throw (SQLException) response;
00396           else
00397             throw new SQLException("Error during connection (received "
00398                 + response + ")");
00399         }
00400         catch (IOException e)
00401         {
00402           currentControllerURL = null;
00403           throw new SQLException("Authentication failed");
00404         }
00405         return setParametersOnConnection(info, new Connection(this, socket, in,
00406             out, url, user, password, needSkeleton, filter));
00407       }
00408       catch (Exception re)
00409       {
00410         controllerConfig.remove(picked);
00411         retry--;
00412         if (retry == 0)
00413         {
00414           if (!sentVdbName)
00415             throw new SQLException("Unable to connect to controller on "
00416                 + currentControllerConfig.getHostname() + ":"
00417                 + currentControllerConfig.getPort() + " (" + re + ")");
00418           else if (!sentUserInfo)
00419             throw new SQLException(
00420                 "Unable to connect to the virtual database (virtual database name is probably not correct)");
00421           else
00422             throw new SQLException(
00423                 "Unable to connect to the virtual database (" + re + ")");
00424         }
00425         else
00426         { // Reset if no more controllers in the list
00427           if (controllerConfig.isEmpty())
00428             currentControllerURL = null;
00429         }
00430       }
00431     }
00432     throw new SQLException(
00433         "Unable to connect to the virtual database - Unexpected error.");
00434   }
00435 
00436   /**
00437    * Tests if the URL is understood by the driver. Calls the
00438    * <code>parseURL()</code> method.
00439    * 
00440    * @param url the JDBC URL.
00441    * @return <code>true</code> if the URL is correct, otherwise an exception
00442    *         with extensive error message is thrown.
00443    * @exception SQLException if the URL is incorrect an explicit error message
00444    *              is given.
00445    */
00446   public synchronized boolean acceptsURL(String url) throws SQLException
00447   {
00448     try
00449     {
00450       parseURL(url);
00451       return true;
00452     }
00453     catch (SQLException e)
00454     {
00455       return false;
00456     }
00457   }
00458 
00459   /**
00460    * Checks for URL correctness and adds controllers list and database name to
00461    * the cache.
00462    * 
00463    * @param url the URL of the C-JDBC controller to which to connect.
00464    * @exception SQLException if an error occurs.
00465    */
00466   synchronized Hashtable parseURL(String url) throws SQLException
00467   {
00468     // Find the hostname and check for URL correctness
00469     if (url == null)
00470     {
00471       throw new IllegalArgumentException(
00472           "Illegal null URL in parseURL(String) method");
00473     }
00474 
00475     if (!url.toLowerCase().startsWith(CJDBC_URL_HEADER))
00476       throw new SQLException("Malformed header from URL '" + url
00477           + "' (expected '" + CJDBC_URL_HEADER + "')");
00478     else
00479     {
00480       // Initialize return
00481       Hashtable result = new Hashtable();
00482 
00483       // Get the controllers list
00484       int nextSlash = url.indexOf('/', CJDBC_URL_HEADER_LENGTH);
00485       if (nextSlash == -1)
00486         // Missing '/' between hostname and database name.
00487         throw new SQLException("Malformed URL '" + url + "' (expected '"
00488             + CJDBC_URL_HEADER + "<hostname>/<database>')");
00489 
00490       // Found end of database name
00491       int questionMark = url.indexOf('?', nextSlash);
00492       questionMark = (questionMark == -1)
00493           ? url.indexOf(';', nextSlash)
00494           : questionMark;
00495 
00496       String controllerURLs = url.substring(CJDBC_URL_HEADER_LENGTH, nextSlash);
00497       // Check the validity of each controller in the list
00498       StringTokenizer controllers = new StringTokenizer(controllerURLs, ",",
00499           true);
00500       int tokenNumber = controllers.countTokens();
00501       ArrayList list = new ArrayList();
00502 
00503       int i = 0;
00504       String s;
00505       boolean lastTokenWasComma = false;
00506       while (controllers.hasMoreTokens())
00507       {
00508         s = controllers.nextToken().trim();
00509         if (s.equals(","))
00510         {
00511           if (lastTokenWasComma || (i == 0) || (i == tokenNumber - 1))
00512             // ',' cannot be the first or the last token
00513             // another ',' cannot follow a ','
00514             throw new SQLException("Syntax error in controller list '"
00515                 + controllerURLs + "' from URL '" + url + "'");
00516           else
00517           {
00518             lastTokenWasComma = true;
00519             continue;
00520           }
00521         }
00522         lastTokenWasComma = false;
00523         list.add(parseController(s));
00524         i++;
00525       }
00526 
00527       ControllerInfo[] controllerList = new ControllerInfo[i];
00528       for (int j = 0; j < i; j++)
00529       {
00530         controllerList[j] = (ControllerInfo) (list.get(j));
00531       }
00532       // Add controller list to the result
00533       result.put(CONTROLLER_PROPERTY, controllerList);
00534 
00535       // Check database name validity
00536       String databaseName = (questionMark == -1) ? url.substring(nextSlash + 1,
00537           url.length()) : url.substring(nextSlash + 1, questionMark);
00538       Character c = validDatabaseName(databaseName);
00539       if (c != null)
00540         throw new SQLException(
00541             "Unable to validate database name (unacceptable character '" + c
00542                 + "' in database '" + databaseName + "' from URL '" + url
00543                 + "')");
00544 
00545       // Add database name to result
00546       result.put(DATABASE_PROPERTY, databaseName);
00547 
00548       // Finally, add the controllers list and database name to the caches
00549       controllerCache.put(url, controllerList);
00550       dbNameCache.put(url, databaseName);
00551 
00552       // Get the parameters from the url
00553       Hashtable params = parseUrlParams(url);
00554       if (params != null)
00555         result.put(PARAMETER_PROPERTY, params);
00556       return result;
00557     }
00558   }
00559 
00560   /**
00561    * Set the different parameters on the connection. Possible values are:
00562    * <code>BOOLEAN_TRUE_PROPERTY</code><br>
00563    * <code>BOOLEAN_FALSE_PROPERTY</code><br>
00564    * <code>ESCAPE_BACKSLASH_PROPERTY</code><br>
00565    * <code>ESCAPE_SINGLE_QUOTE_PROPERTY</code><br>
00566    * 
00567    * @param props the properties used to connect to the controller. These
00568    *          properties should be collected from both the url and the
00569    *          <code>Properties</code> object passed in to the connect method
00570    * @param connection the connection to set the parameters on. Previous
00571    *          parameters will be overriden
00572    * @return the same connection with the parameters set
00573    */
00574   private java.sql.Connection setParametersOnConnection(Properties props,
00575       org.objectweb.cjdbc.driver.Connection connection)
00576   {
00577     String booleanTrue = props.getProperty(BOOLEAN_TRUE_PROPERTY);
00578     if (booleanTrue != null)
00579       connection.setPreparedStatementBooleanTrue(booleanTrue);
00580     String booleanFalse = props.getProperty(BOOLEAN_FALSE_PROPERTY);
00581     if (booleanFalse != null)
00582       connection.setPreparedStatementBooleanFalse(booleanFalse);
00583     String escapeBaskslash = props.getProperty(ESCAPE_BACKSLASH_PROPERTY);
00584     if (escapeBaskslash != null)
00585       connection
00586           .setEscapeBackslash(new Boolean(escapeBaskslash).booleanValue());
00587     String escapeQuote = props.getProperty(ESCAPE_SINGLE_QUOTE_PROPERTY);
00588     if (escapeQuote != null)
00589       connection.setEscapeSingleQuote(new Boolean(escapeQuote).booleanValue());
00590 
00591     String escapeChar = props.getProperty(ESCAPE_CHARACTER_PROPERTY);
00592     if (escapeChar != null)
00593       connection.setEscapeChar(escapeChar);
00594 
00595     String driverProcessed = props.getProperty(DRIVER_PROCESSED_PROPERTY);
00596     if (driverProcessed != null)
00597       connection.setDriverProcessed(Boolean.valueOf(driverProcessed)
00598           .booleanValue());
00599 
00600     return connection;
00601   }
00602 
00603   /**
00604    * Returns the controllerConfig value containing the list of available
00605    * controllers.
00606    * 
00607    * @return Returns the controllerConfig.
00608    */
00609   protected ArrayList getControllerConfig()
00610   {
00611     return controllerConfig;
00612   }
00613 
00614   /**
00615    * Feed in a set of properties and get the url as a <code>String</code>
00616    * 
00617    * @param props as defined statically at the top of the class
00618    * @return <code>String</code> describing the url
00619    */
00620   protected String getUrlFromProperties(Hashtable props)
00621   {
00622     StringBuffer sb = new StringBuffer();
00623     sb.append(CJDBC_URL_HEADER);
00624     ControllerInfo[] controllerList = (ControllerInfo[]) props
00625         .get(CONTROLLER_PROPERTY);
00626     for (int i = 0; i < controllerList.length; i++)
00627     {
00628       if (i == 0)
00629         sb.append(controllerList[i].toString());
00630       else
00631         sb.append("," + controllerList[i].toString());
00632     }
00633     sb.append("/" + props.get(DATABASE_PROPERTY));
00634     Hashtable params = (Hashtable) props.get(PARAMETER_PROPERTY);
00635     if (params != null)
00636     {
00637       Enumeration paramsKeys = params.keys();
00638       String element = null;
00639       while (paramsKeys.hasMoreElements())
00640       {
00641         if (element == null)
00642           sb.append("?");
00643         else
00644           sb.append("&");
00645         element = (String) paramsKeys.nextElement();
00646         sb.append(element + "=" + params.get(paramsKeys));
00647       }
00648     }
00649     return sb.toString();
00650   }
00651 
00652   private Hashtable parseUrlParams(String url) throws SQLException
00653   {
00654     Hashtable props;
00655     props = parseUrlParams(url, '?', "&", "=");
00656     if (props == null)
00657     {
00658       props = parseUrlParams(url, ';', ";", "=");
00659     }
00660     return props;
00661   }
00662 
00663   private Hashtable parseUrlParams(String url, char mark, String link,
00664       String equal) throws SQLException
00665   {
00666     int questionMark = url.indexOf(mark, url.lastIndexOf('/'));
00667     if (questionMark == -1)
00668       return null;
00669     else
00670     {
00671       Hashtable props = new Hashtable();
00672       String parameters = url.substring(questionMark + 1);
00673       StringTokenizer st1 = new StringTokenizer(parameters, link);
00674       while (st1.hasMoreTokens())
00675       {
00676         String param = st1.nextToken();
00677         StringTokenizer st2 = new StringTokenizer(param, equal);
00678         if (st2.hasMoreTokens())
00679         {
00680           try
00681           {
00682             String paramName = st2.nextToken();
00683             String paramValue = (st2.hasMoreTokens()) ? st2.nextToken() : "";
00684             props.put(paramName, paramValue);
00685           }
00686           catch (Exception e)
00687           {
00688             throw new SQLException("Invalid parameter in URL");
00689           }
00690         }
00691       }
00692       return props;
00693     }
00694   }
00695 
00696   /**
00697    * Checks the validity of the hostname, port number and controller name given
00698    * in the URL and build the full URL used to lookup a controller.
00699    * 
00700    * @param controller information regarding a controller.
00701    * @return a <code>ControllerInfo</code> object
00702    * @exception SQLException if an error occurs.
00703    */
00704   private ControllerInfo parseController(String controller) throws SQLException
00705   {
00706     ControllerInfo controllerInfo = new ControllerInfo();
00707 
00708     // Check controller syntax
00709     StringTokenizer controllerURL = new StringTokenizer(controller, ":", true);
00710 
00711     // Get hostname
00712     controllerInfo.setHostname(controllerURL.nextToken());
00713     Character c = validHostname(controllerInfo.getHostname());
00714     if (c != null)
00715       throw new SQLException(
00716           "Unable to validate hostname (unacceptable character '" + c
00717               + "' in hostname '" + controllerInfo.getHostname()
00718               + "' from the URL part '" + controller + "')");
00719 
00720     if (!controllerURL.hasMoreTokens())
00721       controllerInfo.setPort(ControllerConstants.DEFAULT_PORT);
00722     else
00723     {
00724       controllerURL.nextToken(); // should be ':'
00725       if (!controllerURL.hasMoreTokens())
00726         controllerInfo.setPort(ControllerConstants.DEFAULT_PORT);
00727       else
00728       { // Get the port number
00729         String port = controllerURL.nextToken();
00730         if (controllerURL.hasMoreTokens())
00731           throw new SQLException(
00732               "Invalid controller definition with more than one semicolon in URL part '"
00733                   + controller + "'");
00734 
00735         // Check the port number validity
00736         try
00737         {
00738           controllerInfo.setPort(Integer.parseInt(port));
00739         }
00740         catch (NumberFormatException ne)
00741         {
00742           throw new SQLException(
00743               "Unable to validate port number (unacceptable port number '"
00744                   + port + "' in this URL part '" + controller + "')");
00745         }
00746       }
00747     }
00748     return controllerInfo;
00749   }
00750 
00751   /**
00752    * This method is intended to allow a generic GUI tool to discover what
00753    * properties it should prompt a human for in order to get enough information
00754    * to connect to a database.
00755    * <p>
00756    * The only properties supported by C-JDBC are:
00757    * <ul>
00758    * <li>HOST_PROPERTY</li>
00759    * <li>PORT_PROPERTY</li>
00760    * <li>DATABASE_PROPERTY</li>
00761    * <li>USER_PROPERTY</li>
00762    * <li>PASSWORD_PROPERTY</li>
00763    * <li>ESCAPE_CHARACTER_PROPERTY</li>
00764    * <li>ESCAPE_BACKSLASH_PROPERTY</li>
00765    * <li>BOOLEAN_FALSE_PROPERTY</li>
00766    * <li>BOOLEAN_TRUE_PROPERTY</li>
00767    * <li>DRIVER_PROCESSED_PROPERTY</li>
00768    * 
00769    * @param url the URL of the database to connect to
00770    * @param info a proposed list of tag/value pairs that will be sent on connect
00771    *          open.
00772    * @return an array of <code>DriverPropertyInfo</code> objects describing
00773    *         possible properties. This array may be an empty array if no
00774    *         properties are required.
00775    * @exception SQLException if a database-access error occurs.
00776    * @see java.sql.Driver#getPropertyInfo
00777    */
00778   public DriverPropertyInfo[] getPropertyInfo(String url, Properties info)
00779       throws SQLException
00780   {
00781     DriverPropertyInfo hostProp = new DriverPropertyInfo(HOST_PROPERTY, info
00782         .getProperty(HOST_PROPERTY));
00783     hostProp.required = true;
00784     hostProp.description = HOST_PROPERTY_DESCRIPTION;
00785 
00786     DriverPropertyInfo portProp = new DriverPropertyInfo(PORT_PROPERTY, info
00787         .getProperty(PORT_PROPERTY, Integer
00788             .toString(ControllerConstants.DEFAULT_PORT)));
00789     portProp.required = false;
00790     portProp.description = PORT_PROPERTY_DESCRIPTION;
00791 
00792     DriverPropertyInfo databaseProp = new DriverPropertyInfo(DATABASE_PROPERTY,
00793         info.getProperty(DATABASE_PROPERTY));
00794     databaseProp.required = true;
00795     databaseProp.description = DATABASE_PROPERTY_DESCRIPTION;
00796 
00797     DriverPropertyInfo userProp = new DriverPropertyInfo(USER_PROPERTY, info
00798         .getProperty(USER_PROPERTY));
00799     userProp.required = true;
00800     userProp.description = USER_PROPERTY_DESCRIPTION;
00801 
00802     DriverPropertyInfo passwordProp = new DriverPropertyInfo(PASSWORD_PROPERTY,
00803         info.getProperty(PASSWORD_PROPERTY));
00804     passwordProp.required = true;
00805     passwordProp.description = PASSWORD_PROPERTY_DESCRIPTION;
00806 
00807     DriverPropertyInfo escapeCharProp = new DriverPropertyInfo(
00808         ESCAPE_CHARACTER_PROPERTY, info.getProperty(ESCAPE_CHARACTER_PROPERTY));
00809     escapeCharProp.required = false;
00810     escapeCharProp.description = ESCAPE_CHARACTER_PROPERTY_DESCRIPTION;
00811 
00812     DriverPropertyInfo escapeBackProp = new DriverPropertyInfo(
00813         ESCAPE_BACKSLASH_PROPERTY, info.getProperty(ESCAPE_BACKSLASH_PROPERTY));
00814     escapeBackProp.required = false;
00815     escapeBackProp.description = ESCAPE_BACKSLASH_PROPERTY_DESCRIPTION;
00816 
00817     DriverPropertyInfo escapeSingleProp = new DriverPropertyInfo(
00818         ESCAPE_SINGLE_QUOTE_PROPERTY, info
00819             .getProperty(ESCAPE_SINGLE_QUOTE_PROPERTY));
00820     escapeSingleProp.required = false;
00821     escapeSingleProp.description = ESCAPE_SINGLE_QUOTE_PROPERTY_DESCRIPTION;
00822 
00823     DriverPropertyInfo booleanFalseProp = new DriverPropertyInfo(
00824         BOOLEAN_FALSE_PROPERTY, info.getProperty(BOOLEAN_FALSE_PROPERTY));
00825     booleanFalseProp.required = false;
00826     booleanFalseProp.description = BOOLEAN_FALSE_PROPERTY_DESCRIPTION;
00827 
00828     DriverPropertyInfo booleanTrueProp = new DriverPropertyInfo(
00829         BOOLEAN_TRUE_PROPERTY, info.getProperty(BOOLEAN_TRUE_PROPERTY));
00830     booleanTrueProp.required = false;
00831     booleanTrueProp.description = BOOLEAN_TRUE_PROPERTY_DESCRIPTION;
00832 
00833     DriverPropertyInfo parseQueryProp = new DriverPropertyInfo(
00834         DRIVER_PROCESSED_PROPERTY, info.getProperty(DRIVER_PROCESSED_PROPERTY));
00835     escapeSingleProp.required = false;
00836     escapeSingleProp.description = DRIVER_PROCESSED_PROPERTY_DESCRIPTION;
00837 
00838     return new DriverPropertyInfo[]{hostProp, portProp, databaseProp, userProp,
00839         passwordProp, escapeCharProp, escapeBackProp, escapeSingleProp,
00840         booleanFalseProp, booleanTrueProp, parseQueryProp};
00841   }
00842 
00843   /**
00844    * Gets the river's major version number
00845    * 
00846    * @return the driver's major version number
00847    */
00848   public int getMajorVersion()
00849   {
00850     return MAJOR_VERSION;
00851   }
00852 
00853   /**
00854    * Gets the driver's minor version number
00855    * 
00856    * @return the driver's minor version number
00857    */
00858   public int getMinorVersion()
00859   {
00860     return MINOR_VERSION;
00861   }
00862 
00863   /**
00864    * Reports whether the driver is a genuine JDBC compliant driver. A driver may
00865    * only report <code>true</code> here if it passes the JDBC compliance
00866    * tests, otherwise it is required to return <code>false</code>. JDBC
00867    * compliance requires full support for the JDBC API and full support for SQL
00868    * 92 Entry Level. We cannot ensure that the underlying JDBC drivers will be
00869    * JDBC compliant, so it is safer to return <code>false</code>.
00870    * 
00871    * @return always <code>false</code>
00872    */
00873   public boolean jdbcCompliant()
00874   {
00875     return false;
00876   }
00877 
00878   /**
00879    * Checks that the given name contains acceptable characters for a hostname
00880    * name ([0-9][A-Z][a-z][.]).
00881    * 
00882    * @param hostname name to check (caller must check that it is not
00883    *          <code>null</code>).
00884    * @return <code>null</code> if the hostname is acceptable, else the
00885    *         character that causes the fault.
00886    */
00887   private static Character validHostname(String hostname)
00888   {
00889     char[] name = hostname.toCharArray();
00890     int size = hostname.length();
00891     char c;
00892     //boolean lastCharWasPoint = false; // used to avoid '..' in hostname
00893     char lastChar = ' ';
00894 
00895     for (int i = 0; i < size; i++)
00896     {
00897       c = name[i];
00898 
00899       if (c == '.' || c == '-')
00900       {
00901         if (lastChar == '.' || lastChar == '-' || (i == size - 1) || (i == 0))
00902         {
00903           // . or - cannot be the first or the last char of hostname
00904           // hostname cannot contain '..' or '.-' or '-.' or '--'
00905           return new Character(c);
00906         }
00907       }
00908       else
00909       {
00910         if (((c < '0') || (c > 'z') || ((c > '9') && (c < 'A')) || ((c > 'Z') && (c < 'a'))))
00911         {
00912           return new Character(c);
00913         }
00914       }
00915       lastChar = c;
00916     }
00917     return null;
00918   }
00919 
00920   /**
00921    * Checks that the given name contains acceptable characters for a database
00922    * name ([0-9][A-Z][a-z]).
00923    * 
00924    * @param databaseName name to check (caller must check that it is not
00925    *          <code>null</code>).
00926    * @return <code>null</code> if the name is acceptable, else the character
00927    *         that causes the fault.
00928    */
00929   private static Character validDatabaseName(String databaseName)
00930   {
00931     char[] name = databaseName.toCharArray();
00932     int size = databaseName.length();
00933     char c;
00934 
00935     for (int i = 0; i < size; i++)
00936     {
00937       c = name[i];
00938       if ((c < '0') || (c > 'z') || ((c > '9') && (c < 'A'))
00939           || ((c > 'Z') && (c < 'a')))
00940         return new Character(c);
00941     }
00942     return null;
00943   }
00944 
00945   /**
00946    * Controller related information, namely the host name and the port on which
00947    * the controller is running.
00948    * 
00949    * @author <a href="mailto:Emmanuel.Cecchet@inria.fr">Emmanuel Cecchet </a>
00950    * @version 1.0
00951    */
00952   public class ControllerInfo
00953   {
00954     String hostname;
00955     int    port;
00956 
00957     /**
00958      * Creates a ControllerInfo object
00959      */
00960     public ControllerInfo()
00961     {
00962     }
00963 
00964     /**
00965      * Get the hostname where the controller is running
00966      * 
00967      * @return controller hostname
00968      */
00969     public String getHostname()
00970     {
00971       return hostname;
00972     }
00973 
00974     /**
00975      * Get the port number on which the controller is listening.
00976      * 
00977      * @return port number.
00978      */
00979     public int getPort()
00980     {
00981       return port;
00982     }
00983 
00984     /**
00985      * Set the controller hostname.
00986      * 
00987      * @param string hostname to set
00988      */
00989     public void setHostname(String string)
00990     {
00991       hostname = string;
00992     }
00993 
00994     /**
00995      * Set the port number.
00996      * 
00997      * @param port port number
00998      */
00999     public void setPort(int port)
01000     {
01001       this.port = port;
01002     }
01003 
01004     /**
01005      * @see java.lang.Object#toString()
01006      */
01007     public String toString()
01008     {
01009       return hostname + ":" + port;
01010     }
01011   } // ControllerInfo class
01012 
01013 }

Generated on Mon Apr 11 22:01:31 2005 for C-JDBC by  doxygen 1.3.9.1