/*****************************************************************************
FILE:               Oracle7Adaptor.m
LANGUAGE:           Objective-C
SYSTEM:             GNUstep
USER-INTERFACE:     None
DESCRIPTION

    This class is the GNUstep/db adaptor to the Oracle7 databases.
    Its implementation is based on the OCI v7, and should be compatible
    with, both Oracle6, Oracle7 and Oracle8 databases, but won't work
    with Oracle9 and following databases.
    
AUTHORS
    <PJB> Pascal J. Bourguignon <pjb@imaginet.fr>
MODIFICATIONS
    1999/07/24 Creation.
BUGS
    - The name of this adaptor should probably be more specific.

LEGAL
    Written by:  Pascal J. Bourguignon <pjb@imaginet.fr>
    Funds from:  Orange Concept (http://www.orange-concept.com/)
    Date:        August 1999 - February 2000

    This file is part of the Oracle7 Adaptor, GNUstep Database Library.
    The Oracle7 Adaptor allows access to Oracle[678] databases from GDL.

    Copyright (C) 2000 Free Software Foundation, Inc.

    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 2 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-1307 USA
*****************************************************************************/

#include <Oracle7Adaptor.h>
#include <Oracle7Context.h>
#include <Oracle7Context-ForFriends.h>
#include <Oracle7Channel.h>
#include <Oracle7Exceptions.h>
#include <Oracle7SQLExpression.h>
#include <Oracle7Utilities.h>

#include <O2CI7Context.h>


// connectionDictionary keys:
NSString* Oracle7Adaptor_cdUsername=@"userName";
NSString* Oracle7Adaptor_cdPassword=@"password";
NSString* Oracle7Adaptor_cdServerId=@"serverId";
NSString* Oracle7Adaptor_cdHostMachine=@"hostMachine";
NSString* Oracle7Adaptor_cdNLS_LANG=@"NLS_LANG";
// Check normalizedConnectionDictionary: if you add keys here.


// exceptions:
DECLARE_SYMBOL(Oracle7Adaptor_eBadInitializer)
DECLARE_SYMBOL(Oracle7Adaptor_eInternalError)


// -------------------------------------------------------------------------- //

@interface Oracle7Adaptor(Private)


    -(BOOL)oracleTypeIsStringType:(NSString*)externalType;
        /*
            RETURN:     Whether externalType is a string type, ie. one of:
                            - char
                            - char(n)
                            - long
                            - long(n)
                            - varchar2
                            - varchar2(n)
        */

    -(NSString*)quotedStringValue:(NSString*)value;
        /*
            RETURN:     A quoted version of the string value. Every single 
                        quote in value is doubled, and the whole string is 
                        single quoted.

            NOTE:       New-lines are not quoted, since they're well 
                        accepted by Oracle7 on inserts.
        */


    -(NSString*)dateValue:(NSDate*)value;
        /*
            RETURN:     A SQL expression of the date value.

            NOTE:       The returned string is in the form: 
                            TO_DATE('date value','date format')
        */


    -(unsigned int)countCaseInsensitiveString:(NSString*)string
                  isPresentInArray:(NSArray*)array;
        /*
            RETURN:     The number of occurence of string (case insensitively)
                        that are found in array.
        */

    -(NSDictionary*)normalizedConnectionDictionary:(NSDictionary*)connDict;
        /*
            RETURN:     A dictionary with the keys normalized, that is, 
                        capitalized as defined in the Oracle7Adaptor_cd* 
                        strings.
        */

@end // Oracle7Adaptor(Private).


// -------------------------------------------------------------------------- //

@implementation Oracle7Adaptor


// NSObject methods:

    -(id)init
    {
        return([self initWithName:@"Oracle7"]);
    }//init;


    -(void)dealloc
    {
        [super dealloc];
    }//dealloc;


// EOAdaptor methods:

    -(id)initWithName:(NSString*)aName
    {
        self=[super initWithName:aName];
        if(self!=nil){
            validatingConnectionDictionary=NO;
            [O2CI7Context initializeWithMode:OCI_EV_TSF];
        }
        return(self);
    }//initWithName:;



    -(void)setConnectionDictionary:(NSDictionary*)aDictionary
    {
        [super setConnectionDictionary:
                   [self normalizedConnectionDictionary:aDictionary]];
    }//setConnectionDictionary:;


    -(BOOL)hasValidConnectionDictionary
    {
        /*
            Well, we should not need to handle case insensivity here since
            the connection dictionary has been normalized in 
            setConnectionDictionary:, but this came later, and anyway, 
            a subclass could un-normalize the connection dictionary.
            (That's not a time critical method).
        */
        NSArray*          keys;
        EOAdaptorContext* context;
        EOAdaptorChannel* channel;

        // The connectionDictionary must exist.
        if(nil==connectionDictionary){
            return(NO);
        }
        
        keys=[connectionDictionary allKeys];
        
        // Both the username and the password must be present.
        if(1!=[self countCaseInsensitiveString:Oracle7Adaptor_cdUsername
                 isPresentInArray:keys]){
            return(NO);
        }
        if(1!=[self countCaseInsensitiveString:Oracle7Adaptor_cdPassword
                 isPresentInArray:keys]){
            return(NO);
        }
        if(1!=[self countCaseInsensitiveString:Oracle7Adaptor_cdServerId
                        isPresentInArray:keys]){
            return(NO);
        }
        // All the remainder is optional and not needed to make a valid 
        // connection dictionary. However, we check if exatly 0 or 1 exists.
        if(2<=[self countCaseInsensitiveString:Oracle7Adaptor_cdHostMachine
                        isPresentInArray:keys]){
            return(NO);
        }
        if(2<=[self countCaseInsensitiveString:Oracle7Adaptor_cdNLS_LANG
                 isPresentInArray:keys]){
            return(NO);
        }
        /*
            Why this `validatingConnectionDictionary' flag?
            Because this method will be called by the channel in openChannel,
            and this could provoke an infinite recursion. So there...
        */
        if(validatingConnectionDictionary){
            return(YES);
        }
        // Ok, all is well. Let's try to connect.
        validatingConnectionDictionary=YES;
        NS_DURING
            context=[self createAdaptorContext];
            if(context==nil){
                validatingConnectionDictionary=NO;
                NS_VALRETURN(NO);
            }
            channel=[context createAdaptorChannel];
            if(channel==nil){
                validatingConnectionDictionary=NO;
                NS_VALRETURN(NO);
            }
            if([channel openChannel]){
                [channel closeChannel];
                validatingConnectionDictionary=NO;
                NS_VALRETURN(YES);
            }else{
                validatingConnectionDictionary=NO;
                NS_VALRETURN(NO);
            }
        NS_HANDLER
            validatingConnectionDictionary=NO;
            [localException raise];
        NS_ENDHANDLER
    }//hasValidConnectionDictionary;


    -(EOAdaptorContext*)createAdaptorContext
    {
        return([super createAdaptorContext]);
    }//createAdaptorContext;


    -(Class)expressionClass
    {
        return([Oracle7SQLExpression class]);
    }//expressionClass;


    -(Class)adaptorContextClass
    {
        return([Oracle7Context class]);
    }//adaptorContextClass;


    -(Class)adaptorChannelClass
    {
        return([Oracle7Channel class]);
    }//adaptorChannelClass;


    -(BOOL)isValidQualifierType:(NSString*)externalType
    {
        /*
           It seems that only 'long' and 'long raw' external types 
           cannot be used in where clauses (and other conditions).
        */
        NSRange   firstWordRange;
        NSRange   afterFirstWordRange;
        static NSCharacterSet* spaceOrParen=nil;
        if(spaceOrParen==nil){
            spaceOrParen=[[NSCharacterSet 
                            characterSetWithCharactersInString:@" ("] retain];
        }
        firstWordRange.location=0;
        afterFirstWordRange=[externalType rangeOfCharacterFromSet:spaceOrParen];
        if(afterFirstWordRange.length==0){
            firstWordRange.length=[externalType length];
        }else{
            firstWordRange.length=afterFirstWordRange.location;
        }
        return((NSOrderedSame!=[externalType compare:@"long"
                    options:NSCaseInsensitiveSearch range:firstWordRange]));
    }//isValidQualifierType:;


    -(id)formatAttribute:(EOAttribute*)attribute
    {
        // super does what is needed (returns column name).
        return([super formatAttribute:attribute]);
    }//formatAttribute:;



    -(id)formatValue:(id)value forAttribute:(EOAttribute*)attribute
    {
    // SEE: This implementation is insufficient. Let's have a look at Oracle Doc.
        if([self oracleTypeIsStringType:[attribute externalType]]){
            return([self quotedStringValue:value]);
        }else if([value isKindOfClass:[NSDate class]]){
            return([self dateValue:value]);
        }else{
            return([super formatValue:value forAttribute:attribute]);
        }
    }//formatValue:;



// Oracle7Adaptor methods:

    // None. What would be the purpose of an EO adaptor layer if each and
    // every one EOAdaptor subclass  would add it's own specific methods?
    // Let the client use only the EOAdaptor API to access "portably" our
    // database.

@end // Oracle7Adaptor;


// -------------------------------------------------------------------------- //

@implementation Oracle7Adaptor(Private)


    -(BOOL)oracleTypeIsStringType:(NSString*)externalType
    {
        NSRange   firstWordRange;
        NSRange   afterFirstWordRange;
        static NSCharacterSet* spaceOrParen=nil;
        if(spaceOrParen==nil){
            spaceOrParen=[[NSCharacterSet 
                            characterSetWithCharactersInString:@" ("] retain];
        }
        firstWordRange.location=0;
        afterFirstWordRange=[externalType rangeOfCharacterFromSet:spaceOrParen];
        if(afterFirstWordRange.length==0){
            firstWordRange.length=[externalType length];
        }else{
            firstWordRange.length=afterFirstWordRange.location;
        }
        return((NSOrderedSame==[externalType compare:@"char"
                    options:NSCaseInsensitiveSearch range:firstWordRange])
             ||(NSOrderedSame==[externalType compare:@"long"
                    options:NSCaseInsensitiveSearch range:firstWordRange])
             ||(NSOrderedSame==[externalType compare:@"varchar2"
                    options:NSCaseInsensitiveSearch range:firstWordRange]));
    }//oracleTypeIsStringType:;


    -(NSString*)quotedStringValue:(NSString*)value
    {
        NSString*       result;
        unsigned int    blen=0;
        unsigned int    vind=0;
        unsigned int    vlen=[value length];
        unichar*        buffer=(unichar*)malloc(sizeof(unichar)*2*(1+vlen));
        buffer[blen++]=(unichar)'\'';
        while(vind<vlen){
            unichar current=[value characterAtIndex:vind];
            if(current==(unichar)'\''){
                buffer[blen++]=(unichar)'\'';
                buffer[blen++]=(unichar)'\'';
            }else{
                buffer[blen++]=current;             
            }
            vind++;
        }
        buffer[blen++]=(unichar)'\'';
        result=[[NSString alloc]initWithCharacters:buffer length:blen];
        free(buffer);
        return([result autorelease]);
    }//quotedStringValue:;


    -(NSString*)dateValue:(NSDate*)value
    {
        NSString* result;
        static NSTimeZone* GMT=nil;
        if(GMT==nil){
            GMT=[[NSTimeZone timeZoneForSecondsFromGMT:0] retain];
        }
        result=[NSString stringWithFormat:
                @"TO_DATE(%@,'SYYYY/MM/DD HH24:MI/SS')",
                [self quotedStringValue:
                    [value descriptionWithCalendarFormat:@"%Y/%m/%d %H:%M:%S"
                           timeZone:GMT locale:nil]]];
        return(result);
    }//quotedDateValue:;


    -(unsigned int)countCaseInsensitiveString:(NSString*)string
                  isPresentInArray:(NSArray*)array
    {
        unsigned int count=[array count];
        unsigned int sc=0;
        unsigned int i=0;
        while(i<count){
            if([string compare:[array objectAtIndex:i] 
                       options:NSCaseInsensitiveSearch]==NSOrderedSame){
                sc++;
            }
            i++;
        }
        return(sc==1);
    }//countCaseInsensitiveString:isPresentInArray:;



    -(NSDictionary*)normalizedConnectionDictionary:(NSDictionary*)connDict
    {
        static NSString*        normalizedKeys[7]={nil};
        NSArray*                keys;
        unsigned int            index;
        NSEnumerator*           enumerator;
        NSString*               current;
        NSMutableDictionary*    normalized=[NSMutableDictionary 
                                    dictionaryWithCapacity:[connDict count]];

        if(normalizedKeys[0]==nil){
            unsigned int i=1;
            normalizedKeys[i++]=Oracle7Adaptor_cdUsername;
            normalizedKeys[i++]=Oracle7Adaptor_cdPassword;
            normalizedKeys[i++]=Oracle7Adaptor_cdServerId;
            normalizedKeys[i++]=Oracle7Adaptor_cdHostMachine;
            // Don't forget to update the array size if you add a key here!
            normalizedKeys[i++]=nil;
            normalizedKeys[0]=Oracle7Adaptor_cdNLS_LANG;
            // (That's a little kludge to be thread-safe here).
        }

        if(nil==connDict){
            return(nil);
        }
        
        keys=[connDict allKeys];
        enumerator=[keys objectEnumerator];
        while(nil!=(current=[enumerator nextObject])){
            index=0;    
            while(normalizedKeys[index]!=nil){
                if([current compare:normalizedKeys[index] 
                       options:NSCaseInsensitiveSearch]==NSOrderedSame){
                    [normalized setObject:[connDict objectForKey:current]
                                forKey:normalizedKeys[index]];
                    break;
                }
                index++;
            }
            if(normalizedKeys[index]==nil){
                [normalized setObject:[connDict objectForKey:current] 
                            forKey:current];
                NSLog(NSLocalizedStringFromTableInBundle(@"Warning! The key \"%@\" in the connection dictionary is not recognized by the Oracle7Adaptor.\n",@"Oracle7Adaptor",Oracle7Utilities_Bundle(),@"Message given when an unknown key is included in the connection dictionary. [1]:The unknown key."),current);
            }
        }
        return(normalized);
    }//normalizedConnectionDictionary:;


@end // Oracle7Adaptor(Private);

// -------------------------------------------------------------------------- //

/*** Oracle7Adaptor.m                 -- 2000-02-21 07:11:21 -- PJB ***/
