/* -*-objc-*- */ /** Implementation of SQLClientJDBC for GNUStep Copyright (C) 2006 Free Software Foundation, Inc. Written by: Richard Frith-Macdonald Date: August 2006 This file is part of the SQLClient Library. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111 USA. $Date: 2006-06-04 10:19:28 +0100 (Sun, 04 Jun 2006) $ $Revision: 23028 $ */ #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #include "config.h" #define SQLCLIENT_PRIVATE @public #include "SQLClient.h" @interface _JDBCTransaction : SQLTransaction @end #include static NSString *JDBCException = @"SQLClientJDBCException"; /* * Cache connection information */ typedef struct { jobject connection; jmethodID commit; jmethodID rollback; jmethodID prepare; jobject statement; jmethodID executeUpdate; jmethodID executeQuery; jmethodID addBatch; jmethodID clearBatch; jmethodID executeBatch; } JInfo; /* SQLClientJVM shamelessly stolen from JIGS ... written by Nicola Pero * and copyright the Free Software Foundation. */ @interface SQLClientJVM : NSObject { } + (void) startVirtualMachineWithClassPath: (NSString *)classPath libraryPath: (NSString *)libraryPath; + (void) destroyVirtualMachine; + (BOOL) isVirtualMachineRunning; + (NSString *) defaultClassPath; + (NSString *) defaultLibraryPath; + (void) attachCurrentThread; + (void) detachCurrentThread; + (void) registerJavaVM: (JavaVM *)javaVMHandle; @end /* * A fast function to get the (JNIEnv *) variable. */ static JNIEnv *SQLClientJNIEnv (); static JavaVM *SQLClientJavaVM = NULL; /* * Return the (JNIEnv *) associated with the current thread, * or NULL if no java virtual machine is running (or if the thread * is not attached to the JVM). * * NB: This function performs a call. Better use your (JNIEnv *) if * you already have it. * */ JNIEnv *SQLClientJNIEnv () { JNIEnv *penv; if ((*SQLClientJavaVM)->GetEnv (SQLClientJavaVM, (void **)&penv, JNI_VERSION_1_2) == JNI_OK) { return penv; } else { return NULL; } } @implementation SQLClientJVM (GNUstepInternals) + (void) _attachCurrentThread: (NSNotification *)not { [self attachCurrentThread]; } + (void) _detachCurrentThread: (NSNotification *)not { [self detachCurrentThread]; } @end @implementation SQLClientJVM + (void) startVirtualMachineWithClassPath: (NSString *)classPath libraryPath: (NSString *)libraryPath { JavaVMInitArgs jvm_args; JavaVMOption options[32]; int args = 0; jint result; JNIEnv *env; NSString *path; NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; if (SQLClientJavaVM != NULL) { [NSException raise: NSGenericException format: @"Only one Java Virtual Machine " @"can be running at each time"]; } // If we don't pass these options, it assumes they are really @"" if (classPath == nil) { classPath = [self defaultClassPath]; if (classPath == nil) { classPath = @""; } } if (libraryPath == nil) { libraryPath = [self defaultLibraryPath]; if (libraryPath == nil) { libraryPath = @""; } } path = [NSString stringWithFormat: @"-Djava.library.path=%@", libraryPath]; options[args].optionString = strdup([path UTF8String]); options[args++].extraInfo = 0; path = [NSString stringWithFormat: @"-Djava.class.path=%@", classPath]; options[args].optionString = strdup([path UTF8String]); options[args++].extraInfo = 0; path = [NSString stringWithFormat: @"-Xbootclasspath/a:%@", classPath]; options[args].optionString = strdup([path UTF8String]); options[args++].extraInfo = 0; options[args].optionString = "-verbose:class,jni"; options[args++].extraInfo = 0; jvm_args.nOptions = args; jvm_args.version = JNI_VERSION_1_2; jvm_args.options = options; jvm_args.ignoreUnrecognized = JNI_FALSE; result = JNI_CreateJavaVM (&SQLClientJavaVM, (void **)&env, &jvm_args); if (result < 0) { [NSException raise: NSGenericException format: @"Could not start Java Virtual Machine"]; } /* Whenever a thread start or ends, we want to automatically attach or detach it to/from the JVM */ [nc addObserver: self selector: @selector (_attachCurrentThread:) name: NSThreadDidStartNotification object: nil]; [nc addObserver: self selector: @selector (_detachCurrentThread:) name: NSThreadWillExitNotification object: nil]; return; } + (void) destroyVirtualMachine { jint result; if (SQLClientJavaVM == NULL) { [NSException raise: NSGenericException format: @"destroyJVM called without a JVM running"]; } result = (*SQLClientJavaVM)->DestroyJavaVM (SQLClientJavaVM); if (result < 0) { [NSException raise: NSGenericException format: @"Could not destroy Java Virtual Machine"]; } else { SQLClientJavaVM = NULL; } } + (BOOL) isVirtualMachineRunning { if (SQLClientJavaVM == NULL) { return NO; } else { return YES; } } + (NSString *) defaultClassPath { NSDictionary *environment = [[NSProcessInfo processInfo] environment]; return [environment objectForKey: @"CLASSPATH"]; } + (NSString *) defaultLibraryPath { NSDictionary *environment = [[NSProcessInfo processInfo] environment]; return [environment objectForKey: @"LD_LIBRARY_PATH"]; } + (void) attachCurrentThread { static int count = 0; JNIEnv *env; JavaVMAttachArgs args; jint result; if (SQLClientJavaVM == NULL) { /* No JVM - nothing to do */ return; } if (SQLClientJNIEnv () != NULL) { /* The thread is already attached */ return; } { NSAutoreleasePool *pool = [NSAutoreleasePool new]; args.version = JNI_VERSION_1_2; args.name = (char *)[[NSString stringWithFormat: @"GNUstepThread-%d", count] cString]; args.group = NULL; result = (*SQLClientJavaVM)->AttachCurrentThread (SQLClientJavaVM, (void **)&env, &args); [pool release]; } if (result < 0) { [NSException raise: NSGenericException format: @"Could not attach thread to the Java VM"]; } count++; if (count > 100000) { /* Duplicated names shouldn't cause any problem */ count = 0; } return; } + (void) detachCurrentThread { jint result; if (SQLClientJavaVM == NULL) { /* No JVM - nothing to do */ return; } if (SQLClientJNIEnv () == NULL) { /* The thread is not attached */ return; } result = (*SQLClientJavaVM)->DetachCurrentThread (SQLClientJavaVM); if (result < 0) { [NSException raise: NSGenericException format: @"Could not detach thread from the Java VM"]; } return; } + (void) registerJavaVM: (JavaVM *)javaVMHandle { if (javaVMHandle == NULL) { [NSException raise: NSInvalidArgumentException format: @"Trying to register a NULL Java VM"]; } if (SQLClientJavaVM != NULL) { if (javaVMHandle == SQLClientJavaVM) { return; } else { [NSException raise: NSGenericException format: @"Trying to register a Java VM " @"while one is already running"]; } } SQLClientJavaVM = javaVMHandle; // Safety check. If javaVMHandle is invalid, the following will crash // your app. The app would crash anyway later on, so it's better to crash // it here, where it is easier to debug. SQLClientJNIEnv (); return; } @end static jstring JStringFromNSString (JNIEnv *env, NSString *string) { jstring javaString; int length = [string length]; /* We allocate strings of up to 10k on the stack - others using malloc. */ if (length < 10000) { unichar uniString[length]; // Get a unicode representation of the string in the buffer [string getCharacters: uniString]; // Create a java string using the buffer javaString = (*env)->NewString (env, uniString, length); // NB: if javaString is NULL, an exception has been thrown. } else { unichar *uniString; uniString = malloc (sizeof (unichar) * length); [string getCharacters: uniString]; javaString = (*env)->NewString (env, uniString, length); free (uniString); } return javaString; } static NSString* NSStringFromJString (JNIEnv *env, jstring string) { unichar *uniString; jsize length; NSString *gnustepString; // Get a Unicode string from the jstring uniString = (unichar *)(*env)->GetStringChars (env, string, NULL); if (uniString == NULL) { // OutOfMemoryError thrown return NULL; } // Get the Unicode string length length = (*env)->GetStringLength (env, string); // Create a GNUstep string from the Unicode string gnustepString = [NSString stringWithCharacters: uniString length: length]; // Release the temporary string (*env)->ReleaseStringChars (env, string, uniString); return gnustepString; } static NSData * NSDataFromByteArray (JNIEnv *env, jbyteArray array) { NSData *returnData; jbyte *bytes; unsigned length; length = (*env)->GetArrayLength (env, array); bytes = (*env)->GetByteArrayElements (env, array, NULL); if (bytes == NULL) { /* OutOfMemoryError */ return nil; } returnData = [NSData dataWithBytes: bytes length: length]; (*env)->ReleaseByteArrayElements (env, array, bytes, 0); return returnData; } static jbyteArray ByteArrayFromNSData (JNIEnv *env, NSData *data) { const jbyte *bytes; unsigned length; jbyteArray javaArray; length = [data length]; bytes = [data bytes]; javaArray = (*env)->NewByteArray (env, length); if (javaArray == NULL) { /* OutOfMemory exception thrown */ return NULL; } (*env)->SetByteArrayRegion (env, javaArray, 0, length, (jbyte *)bytes); if ((*env)->ExceptionCheck (env)) { /* No reason for this to happen - except a bug in NSData */ return NULL; } return javaArray; } static NSString *JExceptionClear (JNIEnv *env) { NSString *desc = nil; jthrowable exc = (*env)->ExceptionOccurred (env); if (exc != NULL) { static jclass java_lang_Exception = NULL; jmethodID jid = NULL; jstring jstr = NULL; // (*env)->ExceptionDescribe (env); // We need to clear the exception before doing anything else. (*env)->ExceptionClear (env); java_lang_Exception = (*env)->FindClass(env, "java/lang/Exception"); if (java_lang_Exception == NULL) { (*env)->DeleteLocalRef (env, exc); (*env)->ExceptionDescribe (env); desc = @"Could not get global reference to " @"java/lang/Exception to describe exception"; goto done; } jid = (*env)->GetMethodID (env, java_lang_Exception, "getMessage", "()Ljava/lang/String;"); if (jid == NULL) { (*env)->DeleteLocalRef (env, exc); desc = @"Could not get the jmethodID of getMessage" @"of java/lang/Exception to describe exception"; goto done; } if ((*env)->PushLocalFrame (env, 1) < 0) { (*env)->DeleteLocalRef (env, exc); (*env)->ExceptionDescribe (env); desc = @"Could not create enough JNI local references " @"to get a description of the exception"; goto done; } // Get the message jstr = (*env)->CallObjectMethod (env, exc, jid); if ((*env)->ExceptionOccurred (env)) { (*env)->ExceptionDescribe (env); (*env)->PopLocalFrame (env, NULL); desc = @"Exception occurred while getting a description of exception"; goto done; } (*env)->DeleteLocalRef (env, exc); if ((*env)->ExceptionOccurred (env)) { (*env)->ExceptionDescribe (env); (*env)->PopLocalFrame (env, NULL); desc = @"Exception occurred while getting a description of exception"; goto done; } if (jstr == NULL) // Oh oh - something really wrong here { (*env)->PopLocalFrame (env, NULL); desc = @"NULL description of exception"; goto done; } desc = NSStringFromJString (env, jstr); if (desc == nil) { (*env)->PopLocalFrame (env, NULL); desc = @"Exception while converting string of exception"; } (*env)->PopLocalFrame (env, NULL); } done: return desc; } // Throw an exception if one occurred static void JException (JNIEnv *env) { NSString *text = JExceptionClear (env); if (text != nil) { [NSException raise: JDBCException format: @"%@", text]; } } static NSDate* NSDateFromNSString (NSString *s) { NSDate *d; char b[32]; BOOL milliseconds = NO; int l; int i; strcpy(b, [s UTF8String]); l = strlen(b); for (i = 0; i < l; i++) { if (b[i] == '\0') { l = i; break; } } while (l > 0 && isspace(b[l-1])) { l--; } b[l] = '\0'; if (l == 10) { s = [NSString stringWithUTF8String: b]; return [NSCalendarDate dateWithString: s calendarFormat: @"%Y-%m-%d" locale: nil]; } else { int e; /* If it's a simple date (YYYY-MM-DD) append time for start of day. */ if (l == 10) { strcat(b, " 00:00:00 +0000"); l += 15; } i = l; while (i-- > 0) { if (b[i] == '+' || b[i] == '-') { break; } if (b[i] == ':' || b[i] == ' ') { i = 0; break; /* No time zone found */ } } if (i == 0) { /* A date and time without a timezone ... assume gmt */ strcpy(b + l, " +0000"); i = l + 1; l += 6; } e = i; if (isdigit(b[i-1])) { /* * Make space between seconds and timezone. */ memmove(&b[i+1], &b[i], l - i); b[i++] = ' '; b[++l] = '\0'; } /* * Ensure we have a four digit timezone value. */ if (isdigit(b[i+1]) && isdigit(b[i+2])) { if (b[i+3] == '\0') { // Two digit time zone ... append zero minutes b[l++] = '0'; b[l++] = '0'; b[l] = '\0'; } else if (b[i+3] == ':') { // Zone with colon before minutes ... remove it b[i+3] = b[i+4]; b[i+4] = b[i+5]; b[--l] = '\0'; } } /* FIXME ... horrible kludge for timestamps with fractional * second information. Force it to 3 digit millisecond */ while (i-- > 0) { if (b[i] == '.') { milliseconds = YES; i++; if (!isdigit(b[i])) { memmove(&b[i+3], &b[i], e-i); l += 3; memcpy(&b[i], "000", 3); } i++; if (!isdigit(b[i])) { memmove(&b[i+2], &b[i], e-i); l += 2; memcpy(&b[i], "00", 2); } i++; if (!isdigit(b[i])) { memmove(&b[i+1], &b[i], e-i); l += 1; memcpy(&b[i], "0", 1); } i++; break; } } if (i > 0 && i < e) { memmove(&b[i], &b[e], l - e); l -= (e - i); } b[l] = '\0'; if (l == 0) { return nil; } s = [NSString stringWithUTF8String: b]; if (milliseconds == YES) { d = [NSCalendarDate dateWithString: s calendarFormat: @"%Y-%m-%d %H:%M:%S.%F %z" locale: nil]; } else { d = [NSCalendarDate dateWithString: s calendarFormat: @"%Y-%m-%d %H:%M:%S %z" locale: nil]; } return d; } } @interface SQLClientJDBC : SQLClient @end static NSDate *future = nil; static NSNull *null = nil; @implementation SQLClientJDBC static int JDBCDATE = 0; static int JDBCTIME = 0; static int JDBCTIMESTAMP = 0; static int JDBCBOOLEAN = 0; static int JDBCBLOB = 0; static int JDBCBINARY = 0; static int JDBCVARBINARY = 0; static int JDBCLONGVARBINARY = 0; static int JDBCVARCHAR = 0; + (void) initialize { if (future == nil) { JNIEnv *env; jclass jc; jfieldID jf; future = [NSCalendarDate dateWithString: @"9999-01-01 00:00:00 +0000" calendarFormat: @"%Y-%m-%d %H:%M:%S %z" locale: nil]; [future retain]; null = [NSNull null]; [null retain]; [SQLClientJVM startVirtualMachineWithClassPath: nil libraryPath: nil]; env = SQLClientJNIEnv(); jc = (*env)->FindClass(env, "java/sql/Types"); JException (env); jf = (*env)->GetStaticFieldID(env, jc, "DATE", "I"); JException (env); JDBCDATE = (*env)->GetStaticIntField(env, jc, jf); JException (env); jf = (*env)->GetStaticFieldID(env, jc, "TIME", "I"); JException (env); JDBCTIME = (*env)->GetStaticIntField(env, jc, jf); JException (env); jf = (*env)->GetStaticFieldID(env, jc, "TIMESTAMP", "I"); JException (env); JDBCTIMESTAMP = (*env)->GetStaticIntField(env, jc, jf); JException (env); jf = (*env)->GetStaticFieldID(env, jc, "BOOLEAN", "I"); JException (env); JDBCBOOLEAN = (*env)->GetStaticIntField(env, jc, jf); JException (env); jf = (*env)->GetStaticFieldID(env, jc, "BLOB", "I"); JException (env); JDBCBLOB = (*env)->GetStaticIntField(env, jc, jf); JException (env); jf = (*env)->GetStaticFieldID(env, jc, "BINARY", "I"); JException (env); JDBCBINARY = (*env)->GetStaticIntField(env, jc, jf); JException (env); jf = (*env)->GetStaticFieldID(env, jc, "VARBINARY", "I"); JException (env); JDBCVARBINARY = (*env)->GetStaticIntField(env, jc, jf); JException (env); jf = (*env)->GetStaticFieldID(env, jc, "LONGVARBINARY", "I"); JException (env); JDBCLONGVARBINARY = (*env)->GetStaticIntField(env, jc, jf); JException (env); jf = (*env)->GetStaticFieldID(env, jc, "VARCHAR", "I"); JException (env); JDBCVARCHAR = (*env)->GetStaticIntField(env, jc, jf); JException (env); } } /* Disconnect and deallocate all resources used. * Do NOT raise an exception. */ - (void) _backendDisconnect { if (extra != 0) { JNIEnv *env = SQLClientJNIEnv(); JInfo *ji = (JInfo*)extra; jclass jc; jmethodID jm; if ((*env)->PushLocalFrame (env, 16) >= 0) { if (ji->statement != 0) { jc = (*env)->GetObjectClass(env, ji->statement); jm = (*env)->GetMethodID (env, jc, "close", "()V"); if (jm == 0) JExceptionClear(env); else (*env)->CallVoidMethod (env, ji->statement, jm); if (jm == 0) JExceptionClear(env); (*env)->DeleteGlobalRef (env, ji->statement); if (jm == 0) JExceptionClear(env); } if (ji->connection != 0) { jc = (*env)->GetObjectClass(env, ji->connection); jm = (*env)->GetMethodID (env, jc, "close", "()V"); if (jm == 0) JExceptionClear(env); else (*env)->CallVoidMethod (env, ji->connection, jm); if (jm == 0) JExceptionClear(env); (*env)->DeleteGlobalRef (env, ji->connection); if (jm == 0) JExceptionClear(env); } (*env)->PopLocalFrame (env, NULL); } NSZoneFree(NSDefaultMallocZone(), extra); extra = 0; } } - (JInfo*) _backendExtra { return (JInfo*)extra; } - (BOOL) backendConnect { if (extra == 0) { connected = NO; if ([self database] != nil) { NSString *dbase = [self database]; NSRange r; [[self class] purgeConnections: nil]; r = [dbase rangeOfString: @":"]; if (r.length > 0) { NSString *url; NSString *cname; JNIEnv *env; jclass jc; jmethodID jm; jobject jo; url = [dbase substringFromIndex: NSMaxRange(r)]; cname = [dbase substringToIndex: r.location]; env = SQLClientJNIEnv(); if (env == 0) { NSLog(@"Connect to '%@' failed to set up Java runtime", [self name]); return NO; } /* Ensure the driver for the database is loaded. */ cname = [cname stringByReplacingString: @"." withString: @"/"]; if ((*env)->FindClass(env, [cname UTF8String]) == 0) { JExceptionClear (env); NSLog(@"Connect to '%@' failed to load driver '%@'.\n" @"Perhaps you need to set your CLASSPATH environment " @"variable to point to the location of your JDBC library.", [self name], cname); return NO; } if ((*env)->PushLocalFrame (env, 32) < 0) { JExceptionClear (env); [self debug: @"Connect to '%@' failed memory allocation '%@'", [self name], cname]; return NO; } /* Get the driver manager class. */ jc = (*env)->FindClass(env, "java/sql/DriverManager"); if (jc == 0) { JExceptionClear (env); (*env)->PopLocalFrame (env, NULL); NSLog(@"Connect to '%@' failed to load DriverManager\n" @"Perhaps you need to set your CLASSPATH environment " @"variable to point to the location of your JDBC library.", [self name]); return NO; } /* Get the method to get a connection. */ jm = (*env)->GetStaticMethodID(env, jc, "getConnection", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)" "Ljava/sql/Connection;"); if (jm == 0) { JExceptionClear (env); (*env)->PopLocalFrame (env, NULL); [self debug: @"Connect to '%@' failed to get connect method", [self name]]; return NO; } /* Get the new connection object */ jobject js1 = JStringFromNSString(env, url); jobject js2 = JStringFromNSString(env, [self user]); jobject js3 = JStringFromNSString(env, [self password]); /* NSLog(@"CONNECT '%@', '%@', '%@'", NSStringFromJString(env, js1), NSStringFromJString(env, js2), NSStringFromJString(env, js3)); */ jo = (*env)->CallStaticObjectMethod(env, jc, jm, js1, js2, js3); if (jo == 0) { JExceptionClear (env); (*env)->PopLocalFrame (env, NULL); [self debug: @"Connect to '%@' failed to get connection", [self name]]; return NO; } /* Make a reference so it can't be garbage collected. */ jo = (*env)->NewGlobalRef(env, jo); if (jo == 0) { JExceptionClear (env); (*env)->PopLocalFrame (env, NULL); [self debug: @"Connect to '%@' failed to get global ref", [self name]]; return NO; } else { JInfo *ji; ji = NSZoneMalloc(NSDefaultMallocZone(), sizeof(JInfo)); memset(ji, '\0', sizeof(*ji)); extra = ji; NS_DURING { ji->connection = jo; jc = (*env)->GetObjectClass(env, ji->connection); /* Get the method to set autocommit. */ jm = (*env)->GetMethodID(env, jc, "setAutoCommit", "(Z)V"); JException (env); /* Turn off autocommit */ (*env)->CallVoidMethod (env, ji->connection, jm, JNI_FALSE); JException (env); ji->commit = (*env)->GetMethodID (env, jc, "commit", "()V"); JException(env); ji->rollback = (*env)->GetMethodID (env, jc, "rollback", "()V"); JException(env); ji->prepare = (*env)->GetMethodID (env, jc, "prepareStatement", "(Ljava/lang/String;)Ljava/sql/PreparedStatement;"); JException(env); jm = (*env)->GetMethodID (env, jc, "createStatement", "()Ljava/sql/Statement;"); JException(env); jo = (*env)->CallObjectMethod (env, ji->connection, jm); JException(env); ji->statement = (*env)->NewGlobalRef(env, jo); JException(env); jc = (*env)->GetObjectClass(env, ji->statement); ji->executeUpdate = (*env)->GetMethodID (env, jc, "executeUpdate", "(Ljava/lang/String;)I"); JException(env); ji->executeQuery = (*env)->GetMethodID (env, jc, "executeQuery", "(Ljava/lang/String;)Ljava/sql/ResultSet;"); JException(env); (*env)->PopLocalFrame (env, NULL); jc = (*env)->GetObjectClass(env, ji->connection); jm = (*env)->GetMethodID (env, jc, "getMetaData", "()Ljava/sql/DatabaseMetaData;"); JException(env); jo = (*env)->CallObjectMethod (env, ji->connection, jm); JException(env); jc = (*env)->GetObjectClass(env, jo); jm = (*env)->GetMethodID (env, jc, "supportsBatchUpdates", "()Z"); JException(env); if ((*env)->CallBooleanMethod (env, jo, jm) == JNI_TRUE) { jc = (*env)->GetObjectClass(env, ji->statement); ji->addBatch = (*env)->GetMethodID (env, jc, "addBatch", "(Ljava/lang/String;)V"); JException(env); ji->clearBatch = (*env)->GetMethodID (env, jc, "clearBatch", "()V"); JException(env); ji->executeBatch = (*env)->GetMethodID (env, jc, "executeBatch", "()[I"); JException(env); } else { ji->addBatch = 0; ji->clearBatch = 0; ji->executeBatch = 0; } } NS_HANDLER { (*env)->PopLocalFrame (env, NULL); [self _backendDisconnect]; [self debug: @"Connect to '%@' using '%@' problem: %@", [self name], [self database], localException]; return NO; } NS_ENDHANDLER connected = YES; } } else { [self debug: @"Connect to '%@' using '%@' has no class", [self name], [self database]]; return NO; } } else { [self debug: @"Connect to '%@' with no user/password/database configured", [self name]]; } } return connected; } - (void) backendDisconnect { if (extra != 0) { NS_DURING { if ([self isInTransaction] == YES) { [self rollback]; } if ([self debugging] > 0) { [self debug: @"Disconnecting client %@", [self clientName]]; } [self _backendDisconnect]; if ([self debugging] > 0) { [self debug: @"Disconnected client %@", [self clientName]]; } } NS_HANDLER { [self _backendDisconnect]; [self debug: @"Error disconnecting from database (%@): %@", [self clientName], localException]; } NS_ENDHANDLER connected = NO; } } - (NSInteger) backendExecute: (NSArray*)info { NSAutoreleasePool *arp = [NSAutoreleasePool new]; NSString *stmt = [info objectAtIndex: 0]; JNIEnv *env = SQLClientJNIEnv(); JInfo *ji; if ([stmt length] == 0) { [arp release]; [NSException raise: NSInternalInconsistencyException format: @"Statement produced null string"]; } if ((*env)->PushLocalFrame (env, 32) < 0) { JExceptionClear(env); [arp release]; [NSException raise: NSInternalInconsistencyException format: @"No java memory for execute"]; } NS_DURING { jmethodID jm; jobject js; /* * Ensure we have a working connection. */ if ([self connect] == NO) { [NSException raise: SQLException format: @"Unable to connect to '%@' to execute statement %@", [self name], stmt]; } ji = (JInfo*)extra; if ([info count] > 1) { unsigned i; jclass jc; stmt = [stmt stringByReplacingString: @"'?'''?'" withString: @"?"]; js = (*env)->CallObjectMethod (env, ji->connection, ji->prepare, JStringFromNSString(env, stmt)); JException(env); jc = (*env)->GetObjectClass(env, js); JException(env); jm = (*env)->GetMethodID (env, jc, "setBytes", "(I[B)V"); JException(env); for (i = 1; i < [info count]; i++) { (*env)->CallIntMethod (env, js, jm, i, ByteArrayFromNSData(env, [info objectAtIndex: i])); JException(env); } jm = (*env)->GetMethodID (env, jc, "executeUpdate", "()I"); JException(env); (*env)->CallIntMethod (env, js, jm); } else { (*env)->CallIntMethod (env, ji->statement, ji->executeUpdate, JStringFromNSString(env, stmt)); } JException(env); if (_inTransaction == NO) { // Not in a transaction ... commit at once. (*env)->CallVoidMethod (env, ji->connection, ji->commit); JException (env); } (*env)->PopLocalFrame (env, NULL); } NS_HANDLER { if (connected == YES) { if (_inTransaction == NO) { ji = (JInfo*)extra; // Not in a transaction ... rollback to clear error state (*env)->CallVoidMethod (env, ji->connection, ji->rollback); JExceptionClear (env); } (*env)->PopLocalFrame (env, NULL); if ([self debugging] > 0) { [self debug: @"Error executing statement:\n%@\n%@", stmt, localException]; } } [localException retain]; [arp release]; [localException autorelease]; [localException raise]; } NS_ENDHANDLER [arp release]; return -1; } - (NSMutableArray*) backendQuery: (NSString*)stmt recordType: (id)rType listType: (id)lType { NSMutableArray *records = nil; NSAutoreleasePool *arp = [NSAutoreleasePool new]; JNIEnv *env = SQLClientJNIEnv(); JInfo *ji; if ([stmt length] == 0) { [arp release]; [NSException raise: NSInternalInconsistencyException format: @"Statement produced null string"]; } if ((*env)->PushLocalFrame (env, 32) < 0) { JExceptionClear(env); [arp release]; [NSException raise: NSInternalInconsistencyException format: @"No java memory for query"]; } NS_DURING { int fieldCount; jclass resultClass; jobject result; jclass metaDatlType; jobject metaData; jmethodID jm; /* * Ensure we have a working connection. */ if ([self connect] == NO) { [NSException raise: SQLException format: @"Unable to connect to '%@' to run query %@", [self name], stmt]; } ji = (JInfo*)extra; result = (*env)->CallObjectMethod (env, ji->statement, ji->executeQuery, JStringFromNSString(env, stmt)); JException (env); resultClass = (*env)->GetObjectClass(env, result); JException (env); jm = (*env)->GetMethodID (env, resultClass, "getMetaData", "()Ljava/sql/ResultSetMetaData;"); JException (env); metaData = (*env)->CallObjectMethod (env, result, jm); JException (env); metaDatlType = (*env)->GetObjectClass(env, metaData); JException (env); jm = (*env)->GetMethodID (env, metaDatlType, "getColumnCount", "()I"); JException (env); fieldCount = (*env)->CallIntMethod (env, metaData, jm); JException (env); if (fieldCount > 0) { NSString *keys[fieldCount]; int types[fieldCount]; unsigned i; jmethodID next; jmethodID wasNull; jmethodID getBinaryStream; jmethodID getBoolean; jmethodID getBytes; jmethodID getString; /* Get the names of each field */ jm = (*env)->GetMethodID (env, metaDatlType, "getColumnName", "(I)Ljava/lang/String;"); JException (env); for (i = 0; i < fieldCount; i++) { jstring js = (*env)->CallObjectMethod (env, metaData, jm, i+1); JException (env); keys[i] = NSStringFromJString (env, js); } /* Get the types of each field. * We treat most as strings. */ jm = (*env)->GetMethodID (env, metaDatlType, "getColumnType", "(I)I"); JException (env); for (i = 0; i < fieldCount; i++) { int v = (*env)->CallIntMethod (env, metaData, jm, i+1); if (v == JDBCDATE || v == JDBCTIME || v == JDBCTIMESTAMP) { types[i] = JDBCTIMESTAMP; } else if (v == JDBCBOOLEAN) { types[i] = JDBCBOOLEAN; } else if (v == JDBCBLOB || v == JDBCBINARY || v == JDBCVARBINARY || v == JDBCLONGVARBINARY) { types[i] = JDBCBLOB; } else { types[i] = JDBCVARCHAR; } } /* Iterate through the result set */ wasNull = (*env)->GetMethodID (env, resultClass, "wasNull", "()Z"); JException (env); getBinaryStream = (*env)->GetMethodID (env, resultClass, "getBinaryStream", "(I)Ljava/io/InputStream;"); JException (env); getBoolean = (*env)->GetMethodID (env, resultClass, "getBoolean", "(I)Z"); JException (env); getBytes = (*env)->GetMethodID (env, resultClass, "getBytes", "(I)[B"); JException (env); getString = (*env)->GetMethodID (env, resultClass, "getString", "(I)Ljava/lang/String;"); JException (env); next = (*env)->GetMethodID (env, resultClass, "next", "()Z"); JException (env); records = [[lType alloc] initWithCapacity: 2]; while ((*env)->CallBooleanMethod (env, result, next) == JNI_TRUE) { SQLRecord *record; id values[fieldCount]; int j; if ((*env)->PushLocalFrame (env, fieldCount * 2) < 0) { JExceptionClear(env); [arp release]; [NSException raise: NSInternalInconsistencyException format: @"No java memory for query"]; } NS_DURING { for (j = 0; j < fieldCount; j++) { id v = null; if (types[j] == JDBCBOOLEAN) { BOOL b = NO; if ((*env)->CallBooleanMethod (env, result, getBoolean, j+1) == JNI_TRUE) { b = YES; } JException (env); if ((*env)->CallBooleanMethod (env, result, wasNull) == JNI_FALSE) { if (b == YES) { v = @"Y"; } else { v = @"N"; } } JException (env); } else if (types[j] == JDBCTIMESTAMP) { jobject jo; jo = (*env)->CallObjectMethod (env, result, getString, j+1); JException (env); if ((*env)->CallBooleanMethod (env, result, wasNull) == JNI_FALSE) { v = NSStringFromJString(env, jo); v = NSDateFromNSString(v); } JException (env); } else if (types[j] == JDBCBLOB) { jbyteArray jo; jo = (*env)->CallObjectMethod (env, result, getBytes, j+1); JException (env); if ((*env)->CallBooleanMethod (env, result, wasNull) == JNI_FALSE) { v = NSDataFromByteArray(env, jo); } JException (env); } else { jobject jo; jo = (*env)->CallObjectMethod (env, result, getString, j+1); JException (env); if ((*env)->CallBooleanMethod (env, result, wasNull) == JNI_FALSE) { v = NSStringFromJString(env, jo); } JException (env); } values[j] = v; } (*env)->PopLocalFrame (env, NULL); } NS_HANDLER { (*env)->PopLocalFrame (env, NULL); [localException raise]; } NS_ENDHANDLER record = [rType newWithValues: values keys: keys count: fieldCount]; [records addObject: record]; [record release]; } } else { records = [[lType alloc] initWithCapacity: 0]; } (*env)->PopLocalFrame (env, NULL); } NS_HANDLER { NSString *n = [localException name]; if (connected == YES) { (*env)->PopLocalFrame (env, NULL); if ([n isEqual: SQLConnectionException] == YES) { [self disconnect]; } if ([self debugging] > 0) { [self debug: @"Error executing statement:\n%@\n%@", stmt, localException]; } } [records release]; records = nil; [localException retain]; [arp release]; [localException autorelease]; [localException raise]; } NS_ENDHANDLER [arp release]; return [records autorelease]; } - (SQLTransaction*) batch: (BOOL)stopOnFailure { _JDBCTransaction *transaction; transaction = (_JDBCTransaction*)NSAllocateObject([_JDBCTransaction class], 0, NSDefaultMallocZone()); transaction->_owner = [self retain]; transaction->_info = [NSMutableArray new]; transaction->_batch = YES; transaction->_stop = stopOnFailure; return [(SQLTransaction*)transaction autorelease]; } - (void) begin { [lock lock]; if (_inTransaction == NO) { _inTransaction = YES; // Leave us locked so the transaction can't be interfered with } else { [lock unlock]; [NSException raise: NSInternalInconsistencyException format: @"begin used inside transaction"]; } } - (void) commit { [lock lock]; if (_inTransaction == NO) { [lock unlock]; [NSException raise: NSInternalInconsistencyException format: @"commit used outside transaction"]; } NS_DURING { JNIEnv *env = SQLClientJNIEnv(); JInfo *ji = (JInfo*)extra; (*env)->CallVoidMethod (env, ji->connection, ji->commit); JException(env); _inTransaction = NO; [lock unlock]; // Locked at start of -commit [lock unlock]; // Locked by -begin } NS_HANDLER { _inTransaction = NO; [lock unlock]; // Locked at start of -commit [lock unlock]; // Locked by -begin [localException raise]; } NS_ENDHANDLER } - (void) dealloc { [self disconnect]; [super dealloc]; } - (NSString*) quoteString: (NSString *)s { static NSCharacterSet *special = nil; NSMutableString *m; NSRange r; unsigned l; if (special == nil) { NSString *stemp; /* * NB. length of C string is 3, so we include a nul character as a * special. */ stemp = [[NSString alloc] initWithBytes: "'\\" length: 3 encoding: NSASCIIStringEncoding]; special = [NSCharacterSet characterSetWithCharactersInString: stemp]; [stemp release]; [special retain]; } /* * Step through string removing nul characters * and escaping quote characters as required. */ m = [[s mutableCopy] autorelease]; l = [m length]; r = NSMakeRange(0, l); r = [m rangeOfCharacterFromSet: special options: NSLiteralSearch range: r]; while (r.length > 0) { unichar c = [m characterAtIndex: r.location]; if (c == 0) { r.length = 1; [m replaceCharactersInRange: r withString: @""]; l--; } else if (c == '\\') { r.length = 0; [m replaceCharactersInRange: r withString: @"\\"]; l++; r.location += 2; } else { r.length = 0; [m replaceCharactersInRange: r withString: @"'"]; l++; r.location += 2; } r = NSMakeRange(r.location, l - r.location); r = [m rangeOfCharacterFromSet: special options: NSLiteralSearch range: r]; } /* Add quoting around it. */ [m replaceCharactersInRange: NSMakeRange(0, 0) withString: @"'"]; [m appendString: @"'"]; return m; } - (void) rollback { [lock lock]; if (_inTransaction == YES) { _inTransaction = NO; NS_DURING { JNIEnv *env = SQLClientJNIEnv(); JInfo *ji = (JInfo*)extra; (*env)->CallVoidMethod (env, ji->connection, ji->rollback); JException(env); [lock unlock]; // Locked at start of -rollback [lock unlock]; // Locked by -begin } NS_HANDLER { [lock unlock]; // Locked at start of -rollback [lock unlock]; // Locked by -begin [localException raise]; } NS_ENDHANDLER } } - (SQLTransaction*) transaction { _JDBCTransaction *transaction; transaction = (_JDBCTransaction*)NSAllocateObject([_JDBCTransaction class], 0, NSDefaultMallocZone()); transaction->_owner = [self retain]; transaction->_info = [NSMutableArray new]; return [(SQLTransaction*)transaction autorelease]; } @end @implementation _JDBCTransaction - (BOOL) _batchable: (NSArray*)a { unsigned c = [a count]; unsigned i; for (i = 0; i < c; i++) { if ([[a objectAtIndex: i] count] > 1) { return NO; } } return YES; } - (void) _merge: (NSMutableArray*)a { unsigned c = [_info count]; unsigned i; for (i = 0; i < c; i++) { id o = [_info objectAtIndex: i]; if ([o isKindOfClass: [NSArray class]] == YES) { [a addObject: o]; } else { [(_JDBCTransaction*)o _merge: a]; } } } - (void) execute { if (_count > 0) { NSAutoreleasePool *arp = [NSAutoreleasePool new]; SQLClientPool *pool; SQLClient *db; BOOL wrapped = NO; BOOL batched = NO; JNIEnv *env; JInfo *ji; env = SQLClientJNIEnv(); if ((*env)->PushLocalFrame (env, 32) < 0) { JExceptionClear(env); [arp release]; [NSException raise: NSInternalInconsistencyException format: @"No java memory for execute"]; } if ([_owner isKindOfClass: [SQLClientPool class]]) { pool = (SQLClientPool*)_owner; db = [pool provideClient]; } else { pool = nil; db = _owner; } ji = [(SQLClientJDBC*)db _backendExtra]; NS_DURING { NSMutableArray *statements; unsigned numberOfStatements; unsigned statement; NSTimeInterval _duration; NSTimeInterval start = 0.0; /* * Ensure we have a working connection. */ if ([db connect] == NO) { [NSException raise: SQLException format: @"Unable to connect to '%@' to execute transaction %@", [_owner name], self]; } _duration = [db durationLogging]; statements = [NSMutableArray arrayWithCapacity: 100]; [self _merge: statements]; numberOfStatements = [statements count]; if (_duration >= 0) { start = GSTickerTimeNow(); } if ([db isInTransaction] == NO) { wrapped = YES; } if (numberOfStatements > 1 && ji->addBatch != 0 && [self _batchable: statements] == YES) { jintArray ja; jint *array; int status = 0; for (statement = 0; statement < numberOfStatements; statement++) { NSString *stmt = [statements objectAtIndex: statement]; jobject js; js = (*env)->CallObjectMethod(env, ji->statement, ji->addBatch, JStringFromNSString(env, stmt)); JException(env); batched = YES; } ja = (*env)->CallObjectMethod(env, ji->statement, ji->executeBatch); JException(env); array = (*env)->GetIntArrayElements(env, ja, 0); for (statement = 0; statement < numberOfStatements; statement++) { status = array[statement]; if (status < 0 && status != -2) { break; } } (*env)->ReleaseIntArrayElements(env, ja, array, 0); batched = NO; (*env)->CallVoidMethod(env, ji->statement, ji->clearBatch); JException(env); if (statement != numberOfStatements) { [NSException raise: NSGenericException format: @"Statement %d error %d in batch with %@", statement, status, [statements objectAtIndex: statement]]; } } else { /* Not batchable ... execute as a transaction without * batching :-( */ for (statement = 0; statement < numberOfStatements; statement++) { NSArray *info = [statements objectAtIndex: statement]; NSString *stmt = [info objectAtIndex: 0]; unsigned c = [info count]; jmethodID jm; jobject js; if (c == 1) { (*env)->CallIntMethod (env, ji->statement, ji->executeUpdate, JStringFromNSString(env, stmt)); } else { unsigned i; jclass jc; stmt = [stmt stringByReplacingString: @"'?'''?'" withString: @"?"]; js = (*env)->CallObjectMethod (env, ji->connection, ji->prepare, JStringFromNSString(env, stmt)); JException(env); jc = (*env)->GetObjectClass(env, js); JException(env); jm = (*env)->GetMethodID (env, jc, "setBytes", "(I[B)V"); JException(env); /* Get data arguments for statement. */ for (i = 1; i < c; i++) { NSData *data; data = [info objectAtIndex: i]; (*env)->CallIntMethod (env, js, jm, i, ByteArrayFromNSData(env, data)); JException(env); } jm = (*env)->GetMethodID(env, jc, "executeUpdate", "()I"); JException(env); (*env)->CallIntMethod (env, js, jm); } JException(env); } } if (wrapped == YES) { wrapped = NO; (*env)->CallVoidMethod (env, ji->connection, ji->commit); JException(env); } (*env)->PopLocalFrame (env, NULL); db->_lastOperation = GSTickerTimeNow(); if (_duration >= 0) { NSTimeInterval d; d = db->_lastOperation - start; if (d >= _duration) { [db debug: @"Duration %g for transaction %@", d, statements]; } } } NS_HANDLER { if (wrapped == YES) { (*env)->CallVoidMethod (env, ji->connection, ji->rollback); JException(env); } if (batched == YES) { (*env)->CallVoidMethod(env, ji->statement, ji->clearBatch); JException(env); } (*env)->PopLocalFrame (env, NULL); [localException raise]; } NS_ENDHANDLER if (nil != pool) { [pool swallowClient: db]; } [arp release]; } } @end