libs-sqlclient/JDBC.m
Niels Grewe 637653cd0c Fix missing interface warning
git-svn-id: svn+ssh://svn.gna.org/svn/gnustep/libs/sqlclient/trunk@39398 72102866-910b-0410-8b05-ffd578937521
2016-02-19 07:13:22 +00:00

1942 lines
45 KiB
Objective-C

/* -*-objc-*- */
/** Implementation of SQLClientJDBC for GNUStep
Copyright (C) 2006 Free Software Foundation, Inc.
Written by: Richard Frith-Macdonald <rfm@gnu.org>
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 <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSCalendarDate.h>
#import <Foundation/NSCharacterSet.h>
#import <Foundation/NSData.h>
#import <Foundation/NSDate.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSException.h>
#import <Foundation/NSLock.h>
#import <Foundation/NSMapTable.h>
#import <Foundation/NSNotification.h>
#import <Foundation/NSNotification.h>
#import <Foundation/NSNull.h>
#import <Foundation/NSProcessInfo.h>
#import <Foundation/NSString.h>
#import <Foundation/NSThread.h>
#import <Foundation/NSUserDefaults.h>
#import <Foundation/NSValue.h>
#import <Performance/GSTicker.h>
#include "config.h"
#define SQLCLIENT_PRIVATE @public
#include "SQLClient.h"
@interface _JDBCTransaction : SQLTransaction
@end
#include <jni.h>
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