001: /**
002: * com.mckoi.database.jdbc.MDriver 19 Jul 2000
003: *
004: * Mckoi SQL Database ( http://www.mckoi.com/database )
005: * Copyright (C) 2000, 2001, 2002 Diehl and Associates, Inc.
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License
009: * Version 2 as published by the Free Software Foundation.
010: *
011: * This program is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014: * GNU General Public License Version 2 for more details.
015: *
016: * You should have received a copy of the GNU General Public License
017: * Version 2 along with this program; if not, write to the Free Software
018: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
019: *
020: * Change Log:
021: *
022: *
023: */package com.mckoi.database.jdbc;
024:
025: import com.mckoi.database.control.DBConfig;
026: import com.mckoi.database.control.DefaultDBConfig;
027:
028: import java.sql.*;
029: import java.io.*;
030: import java.util.Properties;
031: import java.util.Enumeration;
032: import java.util.StringTokenizer;
033: import java.util.Vector;
034: import java.util.Hashtable;
035: import java.net.URL;
036: import java.net.MalformedURLException;
037:
038: /**
039: * JDBC implementation of the driver for the Mckoi database.
040: * <p>
041: * The url protocol is as follows:<p>
042: * <pre>
043: * For connecting to a remote database server:
044: * jdbc:mckoi:[//hostname[:portnum]/][schema_name/]
045: *
046: * eg. jdbc:mckoi://db.mckoi.com:7009/
047: *
048: * If hostname is not provided then it defaults to localhost.
049: * If portnum is not provided it defaults to 9157.
050: * If schema_name is not provided it defaults to APP.
051: *
052: * To start up a database in the local file system the protocol is:
053: * jdbc:mckoi:local://databaseconfiguration/[schema_name/]
054: *
055: * eg. jdbc:mckoi:local://D:/dbdata/db.conf
056: *
057: * If schema_name is not provided it defaults to APP.
058: *
059: * To create a database in the local file system then you need to supply a
060: * 'create=true' assignment in the URL encoding.
061: *
062: * eg. jdbc:mckoi:local://D:/dbdata/db.conf?create=true
063: * </pre>
064: * <p>
065: * A local database runs within the JVM of this JDBC driver. To boot a
066: * local database, you must include the full database .jar release with
067: * your application distribution.
068: * <p>
069: * For connecting to a remote database using the remote URL string, only the
070: * JDBC driver need be included in the classpath.
071: * <p>
072: * NOTE: This needs to be a light-weight object, because a developer could
073: * generate multiple instances of this class. Making an instance of
074: * 'com.mckoi.JDBCDriver' will create at least two instances of this object.
075: *
076: * @author Tobias Downer
077: */
078:
079: public class MDriver implements Driver {
080:
081: // The major and minor version numbers of the driver. This only changes
082: // when the JDBC communcation protocol changes.
083: static final int DRIVER_MAJOR_VERSION = 1;
084: static final int DRIVER_MINOR_VERSION = 0;
085:
086: // The name of the driver.
087: static final String DRIVER_NAME = "Mckoi JDBC Driver";
088: // The version of the driver as a string.
089: static final String DRIVER_VERSION = "" + DRIVER_MAJOR_VERSION
090: + "." + DRIVER_MINOR_VERSION;
091:
092: // The protocol URL header string that signifies a Mckoi JDBC connection.
093: private static final String mckoi_protocol_url = "jdbc:mckoi:";
094:
095: /**
096: * Set to true when this driver is registered.
097: */
098: private static boolean registered = false;
099:
100: // ----- Static methods -----
101:
102: /**
103: * Static method that registers this driver with the JDBC driver manager.
104: */
105: public synchronized static void register() {
106: if (registered == false) {
107: try {
108: java.sql.DriverManager.registerDriver(new MDriver());
109: registered = true;
110: } catch (SQLException e) {
111: e.printStackTrace();
112: }
113: }
114: }
115:
116: // ----- MDriver -----
117:
118: /**
119: * The timeout for a query in seconds.
120: */
121: static int QUERY_TIMEOUT = Integer.MAX_VALUE;
122:
123: /**
124: * The mapping of the database configuration URL string to the LocalBootable
125: * object that manages the connection. This mapping is only used if the
126: * driver makes local connections (eg. 'jdbc:mckoi:local://').
127: */
128: private Hashtable local_session_map;
129:
130: /**
131: * Constructor is public so that instances of the JDBC driver can be
132: * created by developers.
133: */
134: public MDriver() {
135: local_session_map = new Hashtable();
136: }
137:
138: /**
139: * Given a URL encoded arguments string, this will extract the var=value
140: * pairs and put them in the given Properties object. For example,
141: * the string 'create=true&user=usr&password=passwd' will extract the three
142: * values and put them in the Properties object.
143: */
144: private static void parseEncodedVariables(String url_vars,
145: Properties info) {
146:
147: // Parse the url variables.
148: StringTokenizer tok = new StringTokenizer(url_vars, "&");
149: while (tok.hasMoreTokens()) {
150: String token = tok.nextToken().trim();
151: int split_point = token.indexOf("=");
152: if (split_point > 0) {
153: String key = token.substring(0, split_point)
154: .toLowerCase();
155: String value = token.substring(split_point + 1);
156: // Put the key/value pair in the 'info' object.
157: info.put(key, value);
158: } else {
159: System.err.println("Ignoring url variable: '" + token
160: + "'");
161: }
162: } // while (tok.hasMoreTokens())
163:
164: }
165:
166: /**
167: * Creates a new LocalBootable object that is used to manage the connections
168: * to a database running locally. This uses reflection to create a new
169: * com.mckoi.database.jdbcserver.DefaultLocalBootable object. We use
170: * reflection here because we don't want to make a source level dependency
171: * link to the class. Throws an SQLException if the class was not found.
172: */
173: private static LocalBootable createDefaultLocalBootable()
174: throws SQLException {
175: try {
176: Class c = Class
177: .forName("com.mckoi.database.jdbcserver.DefaultLocalBootable");
178: return (LocalBootable) c.newInstance();
179: } catch (Throwable e) {
180: // A lot of people ask us about this error so the message is verbose.
181: throw new SQLException(
182: "I was unable to find the class that manages local database "
183: + "connections. This means you may not have included the correct "
184: + "library in your classpath. Make sure that either mckoidb.jar "
185: + "is in your classpath or your classpath references the complete "
186: + "Mckoi SQL database class hierarchy.");
187: }
188: }
189:
190: /**
191: * Makes a connection to a local database. If a local database connection
192: * has not been made then it is created here.
193: * <p>
194: * Returns a list of two elements, (DatabaseInterface) db_interface and
195: * (String) database_name.
196: */
197: private synchronized Object[] connectToLocal(String url,
198: String address_part, Properties info) throws SQLException {
199:
200: // If the LocalBootable object hasn't been created yet, do so now via
201: // reflection.
202: String schema_name = "APP";
203: DatabaseInterface db_interface;
204:
205: // Look for the name upto the URL encoded variables
206: int url_start = address_part.indexOf("?");
207: if (url_start == -1) {
208: url_start = address_part.length();
209: }
210:
211: // The path to the configuration
212: String config_path = address_part.substring(8, url_start);
213:
214: // If no config_path, then assume it is ./db.conf
215: if (config_path.length() == 0) {
216: config_path = "./db.conf";
217: }
218:
219: // Substitute win32 '\' to unix style '/'
220: config_path = config_path.replace('\\', '/');
221:
222: // Is the config path encoded as a URL?
223: if (config_path.startsWith("jar:")
224: || config_path.startsWith("file:/")
225: || config_path.startsWith("ftp:/")
226: || config_path.startsWith("http:/")
227: || config_path.startsWith("https:/")) {
228: // Don't do anything - looks like a URL already.
229: } else {
230:
231: // We don't care about anything after the ".conf/"
232: String abs_path;
233: String post_abs_path;
234: int schem_del = config_path.indexOf(".conf/");
235: if (schem_del == -1) {
236: abs_path = config_path;
237: post_abs_path = "";
238: } else {
239: abs_path = config_path.substring(0, schem_del + 5);
240: post_abs_path = config_path.substring(schem_del + 5);
241: }
242:
243: // If the config_path contains the string "!/" then assume this is a jar
244: // file configuration reference. For example,
245: // 'C:/my_db/my_jar.jar!/configs/db.conf'
246:
247: // If the config path is not encoded as a URL, add a 'file:/' preffix
248: // to the path to make it a URL. For example 'C:/my_config.conf" becomes
249: // 'file:/C:/my_config.conf', 'C:/my_libs/my_jar.jar!/configs/db.conf'
250: // becomes 'jar:file:/C:/my_libs/my_jar.jar!/configs/db.conf'
251:
252: int jar_delim_i = abs_path.indexOf("!/");
253: String path_part = abs_path;
254: String rest_part = "";
255: String pre = "file:/";
256: if (jar_delim_i != -1) {
257: path_part = abs_path.substring(0, jar_delim_i);
258: rest_part = abs_path.substring(jar_delim_i);
259: pre = "jar:file:/";
260: }
261:
262: // Does the configuration file exist? Or does the resource that contains
263: // the configuration exist?
264: // We try the file with a preceeding '/' and without.
265: File f = new File(path_part);
266: if (!f.exists() && !path_part.startsWith("/")) {
267: f = new File("/" + path_part);
268: if (!f.exists()) {
269: throw new SQLException("Unable to find file: "
270: + path_part);
271: }
272: }
273: // Construct the new qualified configuration path.
274: config_path = pre + f.getAbsolutePath() + rest_part
275: + post_abs_path;
276: // Substitute win32 '\' to unix style '/'
277: // We do this (again) because on win32 'f.getAbsolutePath()' returns win32
278: // style deliminators.
279: config_path = config_path.replace('\\', '/');
280: }
281:
282: // Look for the string '.conf/' in the config_path which is used to
283: // determine the initial schema name. For example, the connection URL,
284: // 'jdbc:mckoi:local:///my_db/db.conf/TOBY' will start the database in the
285: // TOBY schema of the database denoted by the configuration path
286: // '/my_db/db.conf'
287: int schema_del_i = config_path.toLowerCase().indexOf(".conf/");
288: if (schema_del_i > 0 && schema_del_i + 6 < config_path.length()) {
289: schema_name = config_path.substring(schema_del_i + 6);
290: config_path = config_path.substring(0, schema_del_i + 5);
291: }
292:
293: // The url variables part
294: String url_vars = "";
295: if (url_start < address_part.length()) {
296: url_vars = address_part.substring(url_start + 1).trim();
297: }
298:
299: // Is there already a local connection to this database?
300: String session_key = config_path.toLowerCase();
301: LocalBootable local_bootable = (LocalBootable) local_session_map
302: .get(session_key);
303: // No so create one and put it in the connection mapping
304: if (local_bootable == null) {
305: local_bootable = createDefaultLocalBootable();
306: local_session_map.put(session_key, local_bootable);
307: }
308:
309: // Is the connection booted already?
310: if (local_bootable.isBooted()) {
311: // Yes, so simply login.
312: db_interface = local_bootable.connectToJVM();
313: } else {
314: // Otherwise we need to boot the local database.
315:
316: // This will be the configuration input file
317: InputStream config_in;
318: if (!config_path.startsWith("file:/")) {
319: // Make the config_path into a URL and open an input stream to it.
320: URL config_url;
321: try {
322: config_url = new URL(config_path);
323: } catch (MalformedURLException e) {
324: throw new SQLException("Malformed URL: "
325: + config_path);
326: }
327:
328: try {
329: // Try and open an input stream to the given configuration.
330: config_in = config_url.openConnection()
331: .getInputStream();
332: } catch (IOException e) {
333: throw new SQLException(
334: "Unable to open configuration file. "
335: + "I tried looking at '"
336: + config_url.toString() + "'");
337: }
338: } else {
339: try {
340: // Try and open an input stream to the given configuration.
341: config_in = new FileInputStream(new File(
342: config_path.substring(6)));
343: } catch (IOException e) {
344: throw new SQLException(
345: "Unable to open configuration file: "
346: + config_path);
347: }
348:
349: }
350:
351: // Work out the root path (the place in the local file system where the
352: // configuration file is).
353: File root_path;
354: // If the URL is a file, we can work out what the root path is.
355: if (config_path.startsWith("jar:file:/")
356: || config_path.startsWith("file:/")) {
357:
358: int start_i = config_path.indexOf(":/");
359:
360: // If the config_path is pointing inside a jar file, this denotes the
361: // end of the file part.
362: int file_end_i = config_path.indexOf("!");
363: String config_file_part;
364: if (file_end_i == -1) {
365: config_file_part = config_path
366: .substring(start_i + 2);
367: } else {
368: config_file_part = config_path.substring(
369: start_i + 2, file_end_i);
370: }
371:
372: File absolute_config_file = new File(new File(
373: config_file_part).getAbsolutePath());
374: root_path = new File(absolute_config_file.getParent());
375: } else {
376: // This means the configuration file isn't sitting in the local file
377: // system, so we assume root is the current directory.
378: root_path = new File(".");
379: }
380:
381: // Get the configuration bundle that was set as the path,
382: DefaultDBConfig config = new DefaultDBConfig(root_path);
383: try {
384: config.loadFromStream(config_in);
385: config_in.close();
386: } catch (IOException e) {
387: throw new SQLException(
388: "Error reading configuration file: "
389: + config_path + " Reason: "
390: + e.getMessage());
391: }
392:
393: // Parse the url variables
394: parseEncodedVariables(url_vars, info);
395:
396: boolean create_db = false;
397: boolean create_db_if_not_exist = false;
398: create_db = info.getProperty("create", "").equals("true");
399: create_db_if_not_exist = info.getProperty("boot_or_create",
400: "").equals("true")
401: || info.getProperty("create_or_boot", "").equals(
402: "true");
403:
404: // Include any properties from the 'info' object
405: Enumeration prop_keys = info.keys();
406: while (prop_keys.hasMoreElements()) {
407: String key = prop_keys.nextElement().toString();
408: if (!key.equals("user") && !key.equals("password")) {
409: config.setValue(key, (String) info.get(key));
410: }
411: }
412:
413: // Check if the database exists
414: boolean database_exists = local_bootable
415: .checkExists(config);
416:
417: // If database doesn't exist and we've been told to create it if it
418: // doesn't exist, then set the 'create_db' flag.
419: if (create_db_if_not_exist && !database_exists) {
420: create_db = true;
421: }
422:
423: // Error conditions;
424: // If we are creating but the database already exists.
425: if (create_db && database_exists) {
426: throw new SQLException(
427: "Can not create database because a database already exists.");
428: }
429: // If we are booting but the database doesn't exist.
430: if (!create_db && !database_exists) {
431: throw new SQLException(
432: "Can not find a database to start. Either the database needs to "
433: + "be created or the 'database_path' property of the configuration "
434: + "must be set to the location of the data files.");
435: }
436:
437: // Are we creating a new database?
438: if (create_db) {
439: String username = info.getProperty("user", "");
440: String password = info.getProperty("password", "");
441:
442: db_interface = local_bootable.create(username,
443: password, config);
444: }
445: // Otherwise we must be logging onto a database,
446: else {
447: db_interface = local_bootable.boot(config);
448: }
449: }
450:
451: // Make up the return parameters.
452: Object[] ret = new Object[2];
453: ret[0] = db_interface;
454: ret[1] = schema_name;
455:
456: return ret;
457:
458: }
459:
460: // ---------- Implemented from Driver ----------
461:
462: public Connection connect(String url, Properties info)
463: throws SQLException {
464: // We looking for url starting with this protocol
465: if (!acceptsURL(url)) {
466: // If the protocol not valid then return null as in the spec.
467: return null;
468: }
469:
470: DatabaseInterface db_interface;
471: String default_schema = "APP";
472:
473: int row_cache_size;
474: int max_row_cache_size;
475:
476: String address_part = url.substring(url
477: .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[] ret_list = connectToLocal(url, address_part, info);
485: db_interface = (DatabaseInterface) ret_list[0];
486: default_schema = (String) 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: } else {
493: int port = 9157;
494: String host = "127.0.0.1";
495:
496: // Otherwise we must be connecting remotely.
497: if (address_part.startsWith("//")) {
498:
499: String args_string = "";
500: int arg_part = address_part.indexOf('?', 2);
501: if (arg_part != -1) {
502: args_string = address_part.substring(arg_part + 1);
503: address_part = address_part.substring(0, arg_part);
504: }
505:
506: // System.out.println("ADDRESS_PART: " + address_part);
507:
508: int end_address = address_part.indexOf("/", 2);
509: if (end_address == -1) {
510: end_address = address_part.length();
511: }
512: String remote_address = address_part.substring(2,
513: 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
521: .substring(delim + 1));
522: }
523:
524: // System.out.println("REMOTE_ADDRESS: '" + remote_address + "'");
525:
526: // Schema name?
527: String schema_part = "";
528: if (end_address < address_part.length()) {
529: schema_part = address_part
530: .substring(end_address + 1);
531: }
532: String schema_string = schema_part;
533: int schema_end = schema_part.indexOf('/');
534: if (schema_end != -1) {
535: schema_string = schema_part
536: .substring(0, schema_end);
537: } else {
538: schema_end = schema_part.indexOf('?');
539: if (schema_end != -1) {
540: schema_string = schema_part.substring(0,
541: schema_end);
542: }
543: }
544:
545: // System.out.println("SCHEMA_STRING: '" + schema_string + "'");
546:
547: // Argument part?
548: if (!args_string.equals("")) {
549: // System.out.println("ARGS: '" + args_string + "'");
550: parseEncodedVariables(args_string, info);
551: }
552:
553: // Is there a schema or should we default?
554: if (schema_string.length() > 0) {
555: default_schema = schema_string;
556: }
557:
558: } else {
559: if (address_part.trim().length() > 0) {
560: throw new SQLException("Malformed URL: "
561: + address_part);
562: }
563: }
564:
565: // database_name = address_part;
566: // if (database_name == null || database_name.trim().equals("")) {
567: // database_name = "DefaultDatabase";
568: // }
569:
570: // BUG WORKAROUND:
571: // There appears to be a bug in the socket code of some VM
572: // implementations. With the IBM Linux JDK, if a socket is opened while
573: // another is closed while blocking on a read, the socket that was just
574: // opened breaks. This was causing the login code to block indefinitely
575: // and the connection thread causing a null pointer exception.
576: // The workaround is to put a short pause before the socket connection
577: // is established.
578: try {
579: Thread.sleep(85);
580: } catch (InterruptedException e) { /* ignore */
581: }
582:
583: // Make the connection
584: TCPStreamDatabaseInterface tcp_db_interface = new TCPStreamDatabaseInterface(
585: host, port);
586: // Attempt to open a socket to the database.
587: tcp_db_interface.connectToDatabase();
588:
589: db_interface = tcp_db_interface;
590:
591: // For remote connection, row cache uses more memory.
592: row_cache_size = 4111;
593: max_row_cache_size = 8192000;
594:
595: }
596:
597: // System.out.println("DEFAULT SCHEMA TO CONNECT TO: " + default_schema);
598:
599: // Create the connection object on the given database,
600: MConnection connection = new MConnection(url, db_interface,
601: row_cache_size, max_row_cache_size);
602: // Try and login (throws an SQLException if fails).
603: connection.login(info, default_schema);
604:
605: return connection;
606: }
607:
608: public boolean acceptsURL(String url) throws SQLException {
609: return url.startsWith(mckoi_protocol_url)
610: || url.startsWith(":" + mckoi_protocol_url);
611: }
612:
613: public DriverPropertyInfo[] getPropertyInfo(String url,
614: Properties info) throws SQLException {
615: // Is this for asking for usernames and passwords if they are
616: // required but not provided?
617:
618: // Return nothing for now, assume required info has been provided.
619: return new DriverPropertyInfo[0];
620: }
621:
622: public int getMajorVersion() {
623: return DRIVER_MAJOR_VERSION;
624: }
625:
626: public int getMinorVersion() {
627: return DRIVER_MINOR_VERSION;
628: }
629:
630: public boolean jdbcCompliant() {
631: // Certified compliant? - perhaps one day...
632: return false;
633: }
634:
635: }
|