KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > com > mckoi > database > jdbc > MDriver


1 /**
2  * com.mckoi.database.jdbc.MDriver 19 Jul 2000
3  *
4  * Mckoi SQL Database ( http://www.mckoi.com/database )
5  * Copyright (C) 2000, 2001, 2002 Diehl and Associates, Inc.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * Version 2 as published by the Free Software Foundation.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License Version 2 for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * Version 2 along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19  *
20  * Change Log:
21  *
22  *
23  */

24
25 package com.mckoi.database.jdbc;
26
27 import com.mckoi.database.control.DBConfig;
28 import com.mckoi.database.control.DefaultDBConfig;
29
30 import java.sql.*;
31 import java.io.*;
32 import java.util.Properties JavaDoc;
33 import java.util.Enumeration JavaDoc;
34 import java.util.StringTokenizer JavaDoc;
35 import java.util.Vector JavaDoc;
36 import java.util.Hashtable JavaDoc;
37 import java.net.URL JavaDoc;
38 import java.net.MalformedURLException JavaDoc;
39
40 /**
41  * JDBC implementation of the driver for the Mckoi database.
42  * <p>
43  * The url protocol is as follows:<p>
44  * <pre>
45  * For connecting to a remote database server:
46  * jdbc:mckoi:[//hostname[:portnum]/][schema_name/]
47  *
48  * eg. jdbc:mckoi://db.mckoi.com:7009/
49  *
50  * If hostname is not provided then it defaults to localhost.
51  * If portnum is not provided it defaults to 9157.
52  * If schema_name is not provided it defaults to APP.
53  *
54  * To start up a database in the local file system the protocol is:
55  * jdbc:mckoi:local://databaseconfiguration/[schema_name/]
56  *
57  * eg. jdbc:mckoi:local://D:/dbdata/db.conf
58  *
59  * If schema_name is not provided it defaults to APP.
60  *
61  * To create a database in the local file system then you need to supply a
62  * 'create=true' assignment in the URL encoding.
63  *
64  * eg. jdbc:mckoi:local://D:/dbdata/db.conf?create=true
65  * </pre>
66  * <p>
67  * A local database runs within the JVM of this JDBC driver. To boot a
68  * local database, you must include the full database .jar release with
69  * your application distribution.
70  * <p>
71  * For connecting to a remote database using the remote URL string, only the
72  * JDBC driver need be included in the classpath.
73  * <p>
74  * NOTE: This needs to be a light-weight object, because a developer could
75  * generate multiple instances of this class. Making an instance of
76  * 'com.mckoi.JDBCDriver' will create at least two instances of this object.
77  *
78  * @author Tobias Downer
79  */

80
81 public class MDriver implements Driver {
82
83   // The major and minor version numbers of the driver. This only changes
84
// when the JDBC communcation protocol changes.
85
static final int DRIVER_MAJOR_VERSION = 1;
86   static final int DRIVER_MINOR_VERSION = 0;
87
88   // The name of the driver.
89
static final String JavaDoc DRIVER_NAME = "Mckoi JDBC Driver";
90   // The version of the driver as a string.
91
static final String JavaDoc DRIVER_VERSION =
92           "" + DRIVER_MAJOR_VERSION + "." + DRIVER_MINOR_VERSION;
93
94
95   // The protocol URL header string that signifies a Mckoi JDBC connection.
96
private static final String JavaDoc mckoi_protocol_url = "jdbc:mckoi:";
97
98
99   /**
100    * Set to true when this driver is registered.
101    */

102   private static boolean registered = false;
103
104
105   // ----- Static methods -----
106

107   /**
108    * Static method that registers this driver with the JDBC driver manager.
109    */

110   public synchronized static void register() {
111     if (registered == false) {
112       try {
113         java.sql.DriverManager.registerDriver(new MDriver());
114         registered = true;
115       }
116       catch (SQLException e) {
117         e.printStackTrace();
118       }
119     }
120   }
121
122   // ----- MDriver -----
123

124   /**
125    * The timeout for a query in seconds.
126    */

127   static int QUERY_TIMEOUT = Integer.MAX_VALUE;
128
129   /**
130    * The mapping of the database configuration URL string to the LocalBootable
131    * object that manages the connection. This mapping is only used if the
132    * driver makes local connections (eg. 'jdbc:mckoi:local://').
133    */

134   private Hashtable JavaDoc local_session_map;
135   
136   /**
137    * Constructor is public so that instances of the JDBC driver can be
138    * created by developers.
139    */

140   public MDriver() {
141     local_session_map = new Hashtable JavaDoc();
142   }
143
144   /**
145    * Given a URL encoded arguments string, this will extract the var=value
146    * pairs and put them in the given Properties object. For example,
147    * the string 'create=true&user=usr&password=passwd' will extract the three
148    * values and put them in the Properties object.
149    */

150   private static void parseEncodedVariables(String JavaDoc url_vars, Properties JavaDoc info) {
151
152     // Parse the url variables.
153
StringTokenizer JavaDoc tok = new StringTokenizer JavaDoc(url_vars, "&");
154     while (tok.hasMoreTokens()) {
155       String JavaDoc token = tok.nextToken().trim();
156       int split_point = token.indexOf("=");
157       if (split_point > 0) {
158         String JavaDoc key = token.substring(0, split_point).toLowerCase();
159         String JavaDoc value = token.substring(split_point + 1);
160         // Put the key/value pair in the 'info' object.
161
info.put(key, value);
162       }
163       else {
164         System.err.println("Ignoring url variable: '" + token + "'");
165       }
166     } // while (tok.hasMoreTokens())
167

168   }
169
170   /**
171    * Creates a new LocalBootable object that is used to manage the connections
172    * to a database running locally. This uses reflection to create a new
173    * com.mckoi.database.jdbcserver.DefaultLocalBootable object. We use
174    * reflection here because we don't want to make a source level dependency
175    * link to the class. Throws an SQLException if the class was not found.
176    */

177   private static LocalBootable createDefaultLocalBootable()
178                                                          throws SQLException {
179     try {
180       Class JavaDoc c = Class.forName(
181                       "com.mckoi.database.jdbcserver.DefaultLocalBootable");
182       return (LocalBootable) c.newInstance();
183     }
184     catch (Throwable JavaDoc e) {
185       // A lot of people ask us about this error so the message is verbose.
186
throw new SQLException(
187           "I was unable to find the class that manages local database " +
188           "connections. This means you may not have included the correct " +
189           "library in your classpath. Make sure that either mckoidb.jar " +
190           "is in your classpath or your classpath references the complete " +
191           "Mckoi SQL database class hierarchy.");
192     }
193   }
194   
195   /**
196    * Makes a connection to a local database. If a local database connection
197    * has not been made then it is created here.
198    * <p>
199    * Returns a list of two elements, (DatabaseInterface) db_interface and
200    * (String) database_name.
201    */

202   private synchronized Object JavaDoc[] connectToLocal(String JavaDoc url, String JavaDoc address_part,
203                                         Properties JavaDoc info) throws SQLException {
204
205     // If the LocalBootable object hasn't been created yet, do so now via
206
// reflection.
207
String JavaDoc schema_name = "APP";
208     DatabaseInterface db_interface;
209
210     // Look for the name upto the URL encoded variables
211
int url_start = address_part.indexOf("?");
212     if (url_start == -1) {
213       url_start = address_part.length();
214     }
215
216     // The path to the configuration
217
String JavaDoc config_path = address_part.substring(8, url_start);
218
219     // If no config_path, then assume it is ./db.conf
220
if (config_path.length() == 0) {
221       config_path = "./db.conf";
222     }
223     
224     // Substitute win32 '\' to unix style '/'
225
config_path = config_path.replace('\\', '/');
226
227     // Is the config path encoded as a URL?
228
if (config_path.startsWith("jar:") ||
229         config_path.startsWith("file:/") ||
230         config_path.startsWith("ftp:/") ||
231         config_path.startsWith("http:/") ||
232         config_path.startsWith("https:/")) {
233       // Don't do anything - looks like a URL already.
234
}
235     else {
236
237       // We don't care about anything after the ".conf/"
238
String JavaDoc abs_path;
239       String JavaDoc post_abs_path;
240       int schem_del = config_path.indexOf(".conf/");
241       if (schem_del == -1) {
242         abs_path = config_path;
243         post_abs_path = "";
244       }
245       else {
246         abs_path = config_path.substring(0, schem_del + 5);
247         post_abs_path = config_path.substring(schem_del + 5);
248       }
249       
250       // If the config_path contains the string "!/" then assume this is a jar
251
// file configuration reference. For example,
252
// 'C:/my_db/my_jar.jar!/configs/db.conf'
253

254       // If the config path is not encoded as a URL, add a 'file:/' preffix
255
// to the path to make it a URL. For example 'C:/my_config.conf" becomes
256
// 'file:/C:/my_config.conf', 'C:/my_libs/my_jar.jar!/configs/db.conf'
257
// becomes 'jar:file:/C:/my_libs/my_jar.jar!/configs/db.conf'
258

259       int jar_delim_i = abs_path.indexOf("!/");
260       String JavaDoc path_part = abs_path;
261       String JavaDoc rest_part = "";
262       String JavaDoc pre = "file:/";
263       if (jar_delim_i != -1) {
264         path_part = abs_path.substring(0, jar_delim_i);
265         rest_part = abs_path.substring(jar_delim_i);
266         pre = "jar:file:/";
267       }
268
269       // Does the configuration file exist? Or does the resource that contains
270
// the configuration exist?
271
// We try the file with a preceeding '/' and without.
272
File f = new File(path_part);
273       if (!f.exists() && !path_part.startsWith("/")) {
274         f = new File("/" + path_part);
275         if (!f.exists()) {
276           throw new SQLException("Unable to find file: " + path_part);
277         }
278       }
279       // Construct the new qualified configuration path.
280
config_path = pre + f.getAbsolutePath() + rest_part + post_abs_path;
281       // Substitute win32 '\' to unix style '/'
282
// We do this (again) because on win32 'f.getAbsolutePath()' returns win32
283
// style deliminators.
284
config_path = config_path.replace('\\', '/');
285     }
286
287     // Look for the string '.conf/' in the config_path which is used to
288
// determine the initial schema name. For example, the connection URL,
289
// 'jdbc:mckoi:local:///my_db/db.conf/TOBY' will start the database in the
290
// TOBY schema of the database denoted by the configuration path
291
// '/my_db/db.conf'
292
int schema_del_i = config_path.toLowerCase().indexOf(".conf/");
293     if (schema_del_i > 0 &&
294         schema_del_i + 6 < config_path.length()) {
295       schema_name = config_path.substring(schema_del_i + 6);
296       config_path = config_path.substring(0, schema_del_i + 5);
297     }
298     
299     // The url variables part
300
String JavaDoc url_vars = "";
301     if (url_start < address_part.length()) {
302       url_vars = address_part.substring(url_start + 1).trim();
303     }
304
305     // Is there already a local connection to this database?
306
String JavaDoc session_key = config_path.toLowerCase();
307     LocalBootable local_bootable =
308                            (LocalBootable) local_session_map.get(session_key);
309     // No so create one and put it in the connection mapping
310
if (local_bootable == null) {
311       local_bootable = createDefaultLocalBootable();
312       local_session_map.put(session_key, local_bootable);
313     }
314     
315     // Is the connection booted already?
316
if (local_bootable.isBooted()) {
317       // Yes, so simply login.
318
db_interface = local_bootable.connectToJVM();
319     }
320     else {
321       // Otherwise we need to boot the local database.
322

323       // This will be the configuration input file
324
InputStream config_in;
325       if (!config_path.startsWith("file:/")) {
326         // Make the config_path into a URL and open an input stream to it.
327
URL JavaDoc config_url;
328         try {
329           config_url = new URL JavaDoc(config_path);
330         }
331         catch (MalformedURLException JavaDoc e) {
332           throw new SQLException("Malformed URL: " + config_path);
333         }
334
335         try {
336           // Try and open an input stream to the given configuration.
337
config_in = config_url.openConnection().getInputStream();
338         }
339         catch (IOException e) {
340           throw new SQLException("Unable to open configuration file. " +
341                 "I tried looking at '" + config_url.toString() + "'");
342         }
343       }
344       else {
345         try {
346           // Try and open an input stream to the given configuration.
347
config_in = new FileInputStream(new File(config_path.substring(6)));
348         }
349         catch (IOException e) {
350           throw new SQLException("Unable to open configuration file: " +
351                                  config_path);
352         }
353         
354       }
355
356       // Work out the root path (the place in the local file system where the
357
// configuration file is).
358
File root_path;
359       // If the URL is a file, we can work out what the root path is.
360
if (config_path.startsWith("jar:file:/") ||
361           config_path.startsWith("file:/")) {
362
363         int start_i = config_path.indexOf(":/");
364             
365         // If the config_path is pointing inside a jar file, this denotes the
366
// end of the file part.
367
int file_end_i = config_path.indexOf("!");
368         String JavaDoc config_file_part;
369         if (file_end_i == -1) {
370           config_file_part = config_path.substring(start_i + 2);
371         }
372         else {
373           config_file_part = config_path.substring(start_i + 2, file_end_i);
374         }
375         
376         File absolute_config_file = new File(
377                                new File(config_file_part).getAbsolutePath());
378         root_path = new File(absolute_config_file.getParent());
379       }
380       else {
381         // This means the configuration file isn't sitting in the local file
382
// system, so we assume root is the current directory.
383
root_path = new File(".");
384       }
385
386       // Get the configuration bundle that was set as the path,
387
DefaultDBConfig config = new DefaultDBConfig(root_path);
388       try {
389         config.loadFromStream(config_in);
390         config_in.close();
391       }
392       catch (IOException e) {
393         throw new SQLException("Error reading configuration file: " +
394                                config_path + " Reason: " + e.getMessage());
395       }
396
397       // Parse the url variables
398
parseEncodedVariables(url_vars, info);
399
400       boolean create_db = false;
401       boolean create_db_if_not_exist = false;
402       create_db = info.getProperty("create", "").equals("true");
403       create_db_if_not_exist =
404                        info.getProperty("boot_or_create", "").equals("true") ||
405                        info.getProperty("create_or_boot", "").equals("true");
406
407       // Include any properties from the 'info' object
408
Enumeration JavaDoc prop_keys = info.keys();
409       while (prop_keys.hasMoreElements()) {
410         String JavaDoc key = prop_keys.nextElement().toString();
411         if (!key.equals("user") && !key.equals("password")) {
412           config.setValue(key, (String JavaDoc) info.get(key));
413         }
414       }
415
416       // Check if the database exists
417
boolean database_exists = local_bootable.checkExists(config);
418
419       // If database doesn't exist and we've been told to create it if it
420
// doesn't exist, then set the 'create_db' flag.
421
if (create_db_if_not_exist && !database_exists) {
422         create_db = true;
423       }
424
425       // Error conditions;
426
// If we are creating but the database already exists.
427
if (create_db && database_exists) {
428         throw new SQLException(
429                 "Can not create database because a database already exists.");
430       }
431       // If we are booting but the database doesn't exist.
432
if (!create_db && !database_exists) {
433         throw new SQLException(
434            "Can not find a database to start. Either the database needs to " +
435            "be created or the 'database_path' property of the configuration " +
436            "must be set to the location of the data files.");
437       }
438
439       // Are we creating a new database?
440
if (create_db) {
441         String JavaDoc username = info.getProperty("user", "");
442         String JavaDoc password = info.getProperty("password", "");
443
444         db_interface = local_bootable.create(username, password, config);
445       }
446       // Otherwise we must be logging onto a database,
447
else {
448         db_interface = local_bootable.boot(config);
449       }
450     }
451
452     // Make up the return parameters.
453
Object JavaDoc[] ret = new Object JavaDoc[2];
454     ret[0] = db_interface;
455     ret[1] = schema_name;
456
457     return ret;
458
459   }
460
461
462   // ---------- Implemented from Driver ----------
463

464   public Connection connect(String JavaDoc url, Properties JavaDoc info) throws SQLException {
465     // We looking for url starting with this protocol
466
if (!acceptsURL(url)) {
467       // If the protocol not valid then return null as in the spec.
468
return null;
469     }
470
471     DatabaseInterface db_interface;
472     String JavaDoc default_schema = "APP";
473
474     int row_cache_size;
475     int max_row_cache_size;
476
477     String JavaDoc address_part = url.substring(url.indexOf(mckoi_protocol_url) +
478                                         mckoi_protocol_url.length());
479     // If we are to connect this JDBC to a single user database running
480
// within this JVM.
481
if (address_part.startsWith("local://")) {
482
483       // Returns a list of two Objects, db_interface and database_name.
484
Object JavaDoc[] ret_list = connectToLocal(url, address_part, info);
485       db_interface = (DatabaseInterface) ret_list[0];
486       default_schema = (String JavaDoc) ret_list[1];
487
488       // Internal row cache setting are set small.
489
row_cache_size = 43;
490       max_row_cache_size = 4092000;
491
492     }
493     else {
494       int port = 9157;
495       String JavaDoc host = "127.0.0.1";
496
497       // Otherwise we must be connecting remotely.
498
if (address_part.startsWith("//")) {
499
500         String JavaDoc args_string = "";
501         int arg_part = address_part.indexOf('?', 2);
502         if (arg_part != -1) {
503           args_string = address_part.substring(arg_part + 1);
504           address_part = address_part.substring(0, arg_part);
505         }
506
507 // System.out.println("ADDRESS_PART: " + address_part);
508

509         int end_address = address_part.indexOf("/", 2);
510         if (end_address == -1) {
511           end_address = address_part.length();
512         }
513         String JavaDoc remote_address = address_part.substring(2, end_address);
514         int delim = remote_address.indexOf(':');
515         if (delim == -1) {
516           delim = remote_address.length();
517         }
518         host = remote_address.substring(0, delim);
519         if (delim < remote_address.length() - 1) {
520           port = Integer.parseInt(remote_address.substring(delim + 1));
521         }
522
523 // System.out.println("REMOTE_ADDRESS: '" + remote_address + "'");
524

525         // Schema name?
526
String JavaDoc schema_part = "";
527         if (end_address < address_part.length()) {
528           schema_part = address_part.substring(end_address + 1);
529         }
530         String JavaDoc schema_string = schema_part;
531         int schema_end = schema_part.indexOf('/');
532         if (schema_end != -1) {
533           schema_string = schema_part.substring(0, schema_end);
534         }
535         else {
536           schema_end = schema_part.indexOf('?');
537           if (schema_end != -1) {
538             schema_string = schema_part.substring(0, schema_end);
539           }
540         }
541
542 // System.out.println("SCHEMA_STRING: '" + schema_string + "'");
543

544         // Argument part?
545
if (!args_string.equals("")) {
546 // System.out.println("ARGS: '" + args_string + "'");
547
parseEncodedVariables(args_string, info);
548         }
549
550         // Is there a schema or should we default?
551
if (schema_string.length() > 0) {
552           default_schema = schema_string;
553         }
554
555       }
556       else {
557         if (address_part.trim().length() > 0) {
558           throw new SQLException("Malformed URL: " + address_part);
559         }
560       }
561
562 // database_name = address_part;
563
// if (database_name == null || database_name.trim().equals("")) {
564
// database_name = "DefaultDatabase";
565
// }
566

567       // BUG WORKAROUND:
568
// There appears to be a bug in the socket code of some VM
569
// implementations. With the IBM Linux JDK, if a socket is opened while
570
// another is closed while blocking on a read, the socket that was just
571
// opened breaks. This was causing the login code to block indefinitely
572
// and the connection thread causing a null pointer exception.
573
// The workaround is to put a short pause before the socket connection
574
// is established.
575
try {
576         Thread.sleep(85);
577       }
578       catch (InterruptedException JavaDoc e) { /* ignore */ }
579
580       // Make the connection
581
TCPStreamDatabaseInterface tcp_db_interface =
582                                    new TCPStreamDatabaseInterface(host, port);
583       // Attempt to open a socket to the database.
584
tcp_db_interface.connectToDatabase();
585
586       db_interface = tcp_db_interface;
587
588       // For remote connection, row cache uses more memory.
589
row_cache_size = 4111;
590       max_row_cache_size = 8192000;
591
592     }
593
594 // System.out.println("DEFAULT SCHEMA TO CONNECT TO: " + default_schema);
595

596     // Create the connection object on the given database,
597
MConnection connection = new MConnection(url, db_interface,
598                                         row_cache_size, max_row_cache_size);
599     // Try and login (throws an SQLException if fails).
600
connection.login(info, default_schema);
601
602     return connection;
603   }
604
605   public boolean acceptsURL(String JavaDoc url) throws SQLException {
606     return url.startsWith(mckoi_protocol_url) ||
607            url.startsWith(":" + mckoi_protocol_url);
608   }
609
610   public DriverPropertyInfo[] getPropertyInfo(String JavaDoc url, Properties JavaDoc info)
611                                                           throws SQLException {
612     // Is this for asking for usernames and passwords if they are
613
// required but not provided?
614

615     // Return nothing for now, assume required info has been provided.
616
return new DriverPropertyInfo[0];
617   }
618
619   public int getMajorVersion() {
620     return DRIVER_MAJOR_VERSION;
621   }
622
623   public int getMinorVersion() {
624     return DRIVER_MINOR_VERSION;
625   }
626
627   public boolean jdbcCompliant() {
628     // Certified compliant? - perhaps one day...
629
return false;
630   }
631
632 }
633
Popular Tags