Novell Home

The Anatomy of a Simple IntranetWare Client/Server Application: Part 3

Articles and Tips: article

LAWRENCE V. FISHER
Senior Research Engineer
Developer Information

01 Nov 1997


Third in a series of articles centered on the development of a real client/server application called API_Info. This installment focuses on navigating NDS trees, searching an NDS tree, and connecting and authenticating to a tree to access the service.

Introduction

Novell Developer Information, the source of this publication, recently developed a CBT CD-ROM entitled "IntranetWare Programming Fundamentals." This article is the third and last in a series which takes selections from the CBT and condenses them for presentation to you in this magazine.

The purpose of this series is to give new Novell programmers a wholistic view of IntranetWare developer issues and tasks. To enable that to happen, the discussions in the articles revolve around the development of a simple but real client/server application called API_Info.

This article "Developing a Client," will discuss the following topics as they relate to API_Info:

  • Navigating NDS trees in a multi-tree environment

  • Searching an NDS tree to acquire the optimal service instance for your client

  • Connecting and authenticating to a tree to access the service

During this article, you will learn how to develop the client side of an application like API_Info with the ability to manage tree lists to select a tree, search the selected tree for an optimal service instance, and establish an authenticated connection from the client to the server running the optimal service instance.

Note: The first article of this series discussed how to develop a simple NLM and use an NCP (NetWare Core Protocol) extension to expose the NLM's service on the network. The second article discussed how to extend the NDS schema to accommodate an application's special object types and then how to develop a snapin to enable administrators to manage them.

API_Info Client's Source Code Overview

Figure 1: Components used in the client side of the API_Info application.

API_Info.exe manages the windows in the application and handles all of the user actions in them. We will refer to the code that will be discussed in this article as network code. The network code will handle all network functionality in the client side of the application. The interface between API_Info.exe and the network code is some black box code in an object file called API_GUI.OBJ that is linked with the network code.

Figure 2: dispatchGUIReq Handles Messages from API_GUI.OBJ.

API_GUI.OBJ passes requests and buffers between the .EXE and the dispatchGUIRequest routine in the network code (Figure 2).

The msg2Srvr data structure is used to convey requests from the GUI to the network code or to the NLM. The first member of this structure specifies the request being made. If the request is for an NLM operation, then the second member will contain information needed by the NLM to fulfill the request, the subject API name for example.

In dispatchGUIRequest, the requests from the GUI that require NDS operations (e.g., selecting a new tree or aquiring a service connection) will be handled by the network code discussed in this article. Other requests such as a request to Get Information on an API will be passed on through the connection to the API_Info NLM on the connected server. Once an authenticated, licensed connection to the target server is established, the client side of the API_Info application accesses the API_Info NLM over the connection using the NCP extension method.

Variables for the tree name, user distinguished name, password, and authFlag parameters shown in Figure 2 are defined in the GUI. These parameters have meaning only in the Request Connection message. All tree lists, responses from the NLM, and error strings are sent back to the GUI in the response buffer.

API_Info Client Network Code Source Code Listing

The source code for the API_Info client's network code is listed on the following pages. The discussion following this listing will make frequent references to it.

Note: Routines names beginning with lower-case characters are application defined. Routines in bold are defined in the NetWare SDK.

Includes, Globals, and Type Definitions

/*  Libraries and object files Linked with the network code in API_Info.dll

    CALWIN16.LIB        Contains NetWare APIS called from the client

    NETWIN16.LIB                "

    LOCWIN16.LIB                "

    CLXWIN16.LIB                "

    ALM_NMBR.LIB        Contains graphical user interface routines

    ALM_TEXT.LIB                        "

    ALMKRNL.LIB                 "

    API_GUI.OBJ         (black box GUI code)    

            */



/**********************  Includes  *******************************/

#include "cli_hdr.h"

#include "api_hdr.h"



/************************ Globals*********************************/

NWCONN_HANDLE   gAPIServConn =NULL;// connection to server running API_Info NLM

DWORD   gNCPExtID;// NCP Extension ID on the server running the API_Info NLM



/********************** Type Definitions *************************/

typedef struct

{ // message request from client to server    

     WORD       request;    //  requested operation

     char       data[REQ_BUF_LNGTH];    //  data to server  

} msg2Srvr;

typedef struct

{ // query message reply from server to client

     char       data[REP_BUF_LNGTH];    //  stored message

} msg2Client;

Source for the dispatchGUIReq( ) Routine

/* 

/ The dispatchGUIReq is the central dispatch routine used by the API_Info

/ client code.  It is sent messages by API_GUI.obj in response to user

/ actions in the GUI. The NetWare APIs used in the following code should be

/ good for any platform's NetWare SDK. Since GUI programming is a platform

/ specific task, API_GUI.obj should be considered a black box which sends

/ messages to dispatchGUIReq and displays its responses.  (See Figure ?)

/*/#013;








/********************** dispatchGUIReq ********************************

Receives a request from the API_Info GUI and makes the appropriate call

to fill the response buffer.

*****************************************************************************/

NWCCODE dispatchGUIReq (      char *    requestBuffer,  WORD  reqBfrSize,

                  char *    responseBuffer, WORD *respBfrSize,

                  char *    treeName ,  char *userDName ,

                  char *    passWord,   BOOL    authFlag )

{

    msg2Srvr    *srvrMsgPeek = (msg2Srvr*)requestBuffer;

    if( NWGetClientType() != NW_CLIENT32 )

    {

        strcpy( responseBuffer , "Wrong client type, this app needs Client32!");

        return( (NWCCODE)N_FAILURE );

    }

    switch( (*srvrMsgPeek).request )

    {

        case REQ_INIT:

            return( initNDSEnv( responseBuffer ));



        case REQ_CNCTD_TREE_NMS:

            return( getConnectedTreeNames( responseBuffer ));



        case REQ_CONNECT:   // This is the big one !!

            return( acquireAPIServConn( treeName ,  userDName ,passWord ,

                                authFlag,   responseBuffer ));



        case REQ_ALL_TREE_NMS:

            return( getAllTreeNames( responseBuffer ));



        case REQ_DISCONNECT:        

            return( freeAPIServConn( ));    //found in utilities at end of listing.

        

        case REQ_EXIT:

            return( NWFreeUnicodeTables( ));



        default:    // Requests for service NLM, e.g., GET_LIST, GET_RECORD, etc.

            return( makeNCPExtReq(  requestBuffer,  reqBfrSize,

                        responseBuffer, respBfrSize ));

    }

} /* end of dispatchGUIReq */

Source for Handling the REQ_INIT Message

/* 

/ REQ_INIT is sent to dispatchGUIReq by API_GUI.obj at application startup

/ time to initialize the application.  It is handled by initNDSEnv(), and

/ some less significant utility routines found at the end of this listing.

/*/#013;




/******************************* initNDSEnv ******************************

This function inits NetWare, sets up unicode tables, etc.

***************************************************************************/

BOOL initNDSEnv( char   * responseBuffer )

{

int                     NDSPresent;

NWCCODE                 cCode;

LCONV                   convert;



/* NWCallsInit is called by Window's startup code if client machine already 

    has a connection to the network.  Called again here to confirm it worked */

    cCode = NWCallsInit(NULL, NULL);

    if (cCode)

        return(errorResponse( NULL, "initNDSEnv" , "NWCallsInit" ,

                cCode , responseBuffer));



// Get the locale information and initialize Unicode tables for NDS use

    NWLsetlocale(LC_ALL, "");

    NWLlocaleconv((LCONV NWFAR *)&convert);&
    cCode = NWInitUnicodeTables(convert.country_id, convert.code_page);

    if (cCode)

        return(errorResponse( NULL, "initNDSEnv"" , "NWInitUnicodeTables" ,

                cCode , responseBuffer));



    // Check to see if this is a DS client.

    NDSPresent = NWIsDSAuthenticated();

    if (!NDSPresent)

        return(errorResponse( NULL, "initNDSEnv" , "Not a DS connection..." ,

                N_FAILURE , responseBuffer ));

    return (N_SUCCESS);

} /* end of initNDSEnv */

Source for Handling the REQ_CNCTD_TREE_NMS Message

/* 

/ REQ_CNCTD_TREE_NMS is sent to dispatchGUIReq by API_GUI.obj when the user

/ presses the "Tree" button to obtain a list of the trees that the client is

/ currently connected to.  This message is handled by the following API_Info

/ client routines:  getConnectedTreeNames and some less significant routines

/ that can be found in the utilities section at the end of this listing.

*/





/************************* getConnectedTreeNames **************************

Puts a list of the trees that this client is logged into into the response buffer. 

Upon return, the GUI will display this list for the user .

***************************************************************************/

NWCCODE getConnectedTreeNames( char * responseBuffer )

{

    nuint32             scanIteration = 0 ;

    nuint32             connRef ;

    NWCCConnInfo            prefConnInfoBuf;

    NWCCConnInfo            connRefInfoBuf ;

    NWCONN_HANDLE           connHandle;

    NWRCODE             retCode;

    NWCCODE             cCode;

    char                preferredTreeName[NW_MAX_TREE_NAME_LEN];



    responseBuffer[0]=0;



/*  NWCCScanConnRefs returns a connRef for each connection reference in this

    client's connRef table */

    while( NWCCScanConnRefs( &scanIteration, &connRef )!= NWE_NO_MORE_ENTRIES )&
    {

    /*  The NWCCGetAllConnRefInfo call below fills in the following struct

        for the connection specified in the connRef parameter:

            typedef struct tagNWCCConnInfo

            {

                nuint       authenticationState;

                nuint       broadcastState;

                nuint32     connRef;

                nstr        treeName[NW_MAX_TREE_NAME_LEN];

                nuint       connNum;

                nuint32     userID;

                nstr        serverName[NW_MAX_SERVER_NAME_LEN];

                nuint       NDSState;

                nuint       maxPacketSize;

                nuint       licenseState;

                nuint       distance;

                NWCCVersion serverVersion;

            }   NWCCConnInfo    */



    /* We will get all of the connection reference information, but we will

        just use the treeName and authentication state of the connection */

        retCode = NWCCGetAllConnRefInfo( connRef, NWCC_INFO_VERSION,

                            &connRefInfoBuf );&
        if(retCode)

        {

            return(errorResponse( NULL, "getConnectedTreeNames" ,

                                "NWCCGetAllConnRefInfo", (int)retCode ,

                            responseBuffer  ));

        }



    /* App defined routine simply filters trailing underscore characters. */

        filterNWName( connRefInfoBuf.treeName , '_' );



    /* If the tree name specified by setTreeName's 2nd parameter is already

        in the buffer,setTreeName returns TRUE.  If the third parameter is

        DO_MARK_PREFERRED, setTreeName marks this tree name in the buffer as

        preferred so that the GUI can display it as the preferred tree.

        this routine USES NO NW calls! */

        setTreeName(        responseBuffer,

                    connRefInfoBuf.treeName,

                    DONT_MARK_PREFERRED,

                    (connRefInfoBuf.authenticationState

                    &NWCC_AUTHENT_STATE_NDS));&
    }



/* Get connection to the preferred server to determine which tree is the

   preferred tree. */

    cCode = NWGetPreferredDSServer( &connHandle );&
    if(cCode)

    {

        if(cCode == PREFERRED_NOT_FOUND||cCode == UNKNOWN_FILE_SERVER )

            strcpy( prefConnInfoBuf.treeName, "" );

        else

            return(errorResponse( NULL, "getConnectedTreeNames",

                        "NWGetPreferredDSServer", cCode, responseBuffer));

    }

    else

    {

        retCode = NWCCGetConnInfo( connHandle, NWCC_INFO_TREE_NAME,

                        NW_MAX_TREE_NAME_LEN, preferredTreeName );

        if(retCode)

            return(errorResponse( NULL,"getConnectedTreeNames","NWCCGetConnInfo",

                        (int)retCode , responseBuffer  ));

        else

           filterNWName( preferredTreeName , '_' ); // utility gets rid of '_'s

    }



    if(preferredTreeName[0]!=0)

       setTreeName(     responseBuffer ,

                preferredTreeName ,

                DO_MARK_PREFERRED,

                DONT_MARK_AUTHENTICATED );



    return( (NWCCODE)retCode );

} /* end of getConnectedTreeNames */

Source for Handling the REQ_CONNECT Message

/* 

/ REQ_CONNECT is sent to dispatchGUIReq by API_GUI.obj when the user has

/ selected a new tree name from a tree name list and selects the "Request

/ Service Connection" button.In response to this message, the code below will:

/ 1.Search the selected tree for API_Info NDS objects.

/ 2.Select the API_Info NDS object that references the closest server.

/ 3.Log in to the server referenced by the server name attribute of the

/ selected NDS object and commence NCP interactions with the API_Info NLM

/ running on it. 

/ This message is handled by the following routines: acquireAPIServConn,

/ setupNDSEnv, schemaExtended, getOptimalObjInfo, defineSearch,

/ selectOptimalObj and some less significant routines that can be found

/ in the utilities section at the end of this listing.

/*/#013;




/************************* acquireAPIServConn ******************************

Sets up a context for the client connection, calls getOptimalObjInfo() to determine 

which server to connect to, and then authenticates to the server. Gets the NCP 

Extension ID to be used in all future NCP Extension requests.  Returns N_SUCCESS if 

no errors occur.

***************************************************************************/

NWCCODE acquireAPIServConn (        char *  treeName ,

                        char *  userDName ,

                        char *  passWord ,

                        BOOL        backgdAuthenticate,

                        char *  responseBuffer )

{

    NWCCODE         cCode;

    BYTE            majorVersion;

    BYTE            minorVersion;

    BYTE            revision;

    BYTE            queryData[32];

    NWDSContextHandle   userCntxtHndl;

    char            serverDName[MAX_DN_CHARS];

    char            operatorDName[MAX_DN_CHARS];

    char            userName[MAX_DN_CHARS];

    char            userContext[MAX_DN_CHARS];





   strcpy(serverDName, "[Root]");

   strcpy(operatorDName, "[Root]");



/* parseDistinguishedName is a trivial routine to parse up the distinguished

    name into user name and context.  No NetWare calls made in this routine */

    parseDistinguishedName( userDName , userName , userContext );



//  setupNDSEnv sets up contexts for NDS calls to this new tree

    if ( setupNDSEnv( &userCntxtHndl, userContext, treeName,&
                        responseBuffer ) != N_SUCCESS)

    {

        freeUserContext(userCntxtHndl);

        return((NWCCODE)N_FAILURE);

    }



// This routine makes the actual NWDS calls necessary to search for the

// closest API_Info object

    if( getOptimalObjInfo(  userCntxtHndl, serverDName, operatorDName,

                    responseBuffer ) != N_SUCCESS)

    {

        freeUserContext(userCntxtHndl);

        return((NWCCODE)N_FAILURE);

    }





/* Open the server connection ID to use in NetWare APIs and authenticating

    to the network.  NWDSOpenConnToNDSServer allows you to obtain a connection

    to a specific server that is specified by an NDS distinguished name.

    The serverName is resolved using NDS and a connection is established, or, 

    if the workstation already has a connection, a handle is returned. */

    cCode = NWDSOpenConnToNDSServer(        userCntxtHndl,      // context handle

                            serverDName,        // server name

                            &gAPIServConn );      // conn handle&
    if (cCode)

    {

        freeUserContext(userCntxtHndl);

        freeAPIServConn();

        return(errorResponse( NULL, "acquireAPIServConn" ,

                    "NWDSOpenConnToNDSServer" , cCode ,

                    responseBuffer));

    }



    if(backgdAuthenticate) // if client already has a connection on the tree

    {

        cCode = NWDSAuthenticateConn(userCntxtHndl, gAPIServConn);

        if(cCode)

        {

            freeUserContext(userCntxtHndl);

            freeAPIServConn();

            return(errorResponse( NULL, "acquireAPIServConn" ,

                        "NWDSAuthenticateConn", cCode ,responseBuffer));

        }

    }

    else

    {

    /*      Even though you do not pass the connection handle you received from

        NWDSOpenConnToNDSServer, NWDSLogin uses latest connection which is

        contained in the current context and uses it to authenticate to NDS.

        In this way, we are authenticated to the server we had specified in

        NWDSOpenConnToNDSServer.  We do not need to call NWDSAuthenticateConn

        in this case.*/

        cCode = NWDSLogin (userCntxtHndl, 0L, userName, passWord, 420L);

        if (cCode)

        {

            switch(cCode)

            {

                case (NWCCODE)ERR_FAILED_AUTHENTICATION:

                {

                    errorResponse( "Login failed--access has been denied.  Make

                            sure you type in the correct password.",

                            "acquireAPIServConn" , "NWDSLogin" ,

                            cCode , responseBuffer);

                    break;

                }

                case 0x889A:

                {

                    errorResponse( "User object does not exist in the specified

                            context.  Make sure you type in a valid,

                            distinguished user name.", "acquireAPIServConn",

                            "NWDSLogin" , cCode , responseBuffer);

                    break;

                }

                case DIFF_OBJECT_ALREADY_AUTHEN:

                {

                    errorResponse( "User name conflict! If you have previously

                                logged into the tree as a different user, and

                                you wish to log in as a new user, you must first

                                explicitly log out of the tree--then log back in

                                as the new user.", "acquireAPIServConn" ,

                                "NWDSLogin" , cCode , responseBuffer);

                    break;

                }

                default:

                {

                    errorResponse( NULL, "acquireAPIServConn" , "NWDSLogin" ,

                            cCode , responseBuffer);

                    break;

                }

            }

            freeUserContext(userCntxtHndl);

            freeAPIServConn();

            return (cCode);

        }

    }



//  Make sure NCP Extension server is loaded and get the NCP Extension ID

    cCode = NWGetNCPExtensionInfoByName (       gAPIServConn,

                                NS_NCPEXTENT,

                                &gNCPExtID,&
                                &majorVersion,&
                                &minorVersion,&
                                &revision,&
                                queryData);

    if ( cCode == 0x89FE || cCode == 0x89FF )  //if NCP Extension is not loaded

    {

        cCode = getOperatorInfo (   userCntxtHndl, serverDName, operatorDName,

                            responseBuffer);

        freeUserContext(userCntxtHndl);

        freeAPIServConn();

        return (cCode);

    }

    else if (cCode)

    {

        freeUserContext(userCntxtHndl);

        freeAPIServConn();

        return(errorResponse( NULL, "acquireAPIServConn" ,

                        "NWGetNCPExtensionInfoByName" ,

                    cCode , responseBuffer));

    }



    freeUserContext(userCntxtHndl);

    return (N_SUCCESS);

} /* end of acquireAPIServConn */





/***************************** setupNDSEnv *****************************

This function sets up the user's context for NDS requests and makes a call to 

determine if the current tree's NDS schema is extended for API_Info objects.

***************************************************************************/

BOOL setupNDSEnv    (   NWDSContextHandle * userCntxtHndl,

                char    * userContext,

                char    * treeName,

                char    * responseBuffer)

{

    NWDSCCODE               cCode;



    cCode = NWDSCreateContextHandle(userCntxtHndl);

    if (cCode)

    {

        return(errorResponse( NULL, "setupNDSEnv" , "NWDSCreateContextHandle" ,

                    cCode , responseBuffer));

    }

    else

    {

        if( userContext )

        {   // If userContext not null, set context's name context to it

            cCode = NWDSSetContext(*userCntxtHndl, DCK_NAME_CONTEXT,userContext);

            if (cCode)

            {

                return(errorResponse( NULL, "setupNDSEnv" , "NWDSSetContext" ,

                                        cCode , responseBuffer));

            }

        }

        else

        {   // If null, set name context to default valid distinguished name

            cCode = NWDSSetContext( *userCntxtHndl, DCK_NAME_CONTEXT, "[Root]" );

            if(cCode!=N_SUCCESS)

            {

                return(errorResponse( NULL, "setupNDSEnv" , "NWDSSetContext" ,

                            cCode , responseBuffer));

            }

        }

    }



    if(treeName)

    {   // Set context to refer to target tree

        cCode = NWDSSetContext( *userCntxtHndl, DCK_TREE_NAME, treeName );

        if(cCode!=N_SUCCESS)

        {

            return(errorResponse( NULL, "setupNDSEnv" , "NWDSSetContext" ,

                                    cCode , responseBuffer));

        }

        else

        {   // Is target tree extended for API_Info?

            if (!schemaExtended(*userCntxtHndl, responseBuffer))

                return (N_FAILURE);

        }

    }

    return(N_SUCCESS);

} /* end of setupNDSEnv */







/************************* schemaExtended ***************************

This function determines if the schema of the tree in the given context has been 

extended for API_Info objects.  It checks to see if the attribute definition for the 

APIInfo Server attribute exists.

***************************************************************************/

BOOL schemaExtended ( NWDSContextHandle  userCntxtHndl, char * responseBuffer)

{

    NWDSCCODE               cCode;

    pBuf_T                  extendAttrName=NULL;

    pBuf_T                  attrDef=NULL;

    NWDS_ITERATION       itHandle;  // NWDS_ITERATION is an int32



/* Allocate an NDS buffer to be used in a read operation to determine if

    one of the API_Info extended attributes exists in the schema. */

    cCode = NWDSAllocBuf(DEFAULT_MESSAGE_LEN, &extendAttrName);&
    if(cCode)

    {

        errorResponse( NULL, "schemaExtended" , "NWDSAllocBuf" ,

                    cCode , responseBuffer );

        freeAttrDefBuffers (extendAttrName, attrDef);

        return(FALSE);

    }



    cCode = NWDSInitBuf(userCntxtHndl,  DSV_READ_ATTR_DEF, extendAttrName);

    if(cCode)

    {

        errorResponse( NULL, "schemaExtended" , "NWDSInitBuf" ,

                    cCode , responseBuffer );

        freeAttrDefBuffers (extendAttrName, attrDef);

        return(FALSE);

    }



    cCode = NWDSPutAttrName(    userCntxtHndl,

                    extendAttrName,

                    APIINFOSERVER);

    if(cCode)

    {

        errorResponse( NULL, "schemaExtended" , "NWDSPutAttrName" ,

                    cCode , responseBuffer );

        freeAttrDefBuffers (extendAttrName, attrDef);

        return(FALSE);

    }



// Allocate a buffer to contain the attribute definition if it exists.

    cCode = NWDSAllocBuf(DEFAULT_MESSAGE_LEN, &attrDef);&
    if(cCode)

    {

        errorResponse( NULL, "schemaExtended" , "NWDSAllocBuf" ,

                    cCode , responseBuffer );

        freeAttrDefBuffers (extendAttrName, attrDef);

        return(FALSE);

    }



    itHandle = NO_MORE_ITERATIONS;



    do

    {

        cCode = NWDSReadAttrDef (   userCntxtHndl,

                        DS_ATTR_DEF_NAMES,

                        FALSE,

                        extendAttrName,

                        &itHandle,&
                        attrDef);

        if(cCode)

        {

            errorResponse( "The selected tree's NDS schema has NOT been extended

                    for API_Info.  Click the Tree button to select

                    another tree.", "schemaExtended" , "NWDSReadAttrDef",

                    cCode , responseBuffer );

            freeAttrDefBuffers (extendAttrName, attrDef);

            return(FALSE);

        }

    } while (itHandle != NO_MORE_ITERATIONS );



    freeAttrDefBuffers (extendAttrName, attrDef);

    return (TRUE);

} /* end of schemaExtended */







/*********************** getOptimalObjInfo ********************************

Directs the flow of searching the Directory tree.

***************************************************************************/

BOOL    getOptimalObjInfo   (       NWDSContextHandle   userCntxtHndl,

                        char *          serverDName,

                        char *          operatorDName,

                        char *          responseBuffer  )

{

//  NWDS_NUM_OBJ is an int32

    NWDS_NUM_OBJ            countObjectsSearched;

    int             retValue;

    NWDSCCODE                   cCode;

    nuint               bestResponseTime=0xFFFF;

    pBuf_T              filterBuffer=NULL;

    pBuf_T              foundObjectsBfr=NULL;

    pBuf_T              attrNames=NULL;

    NWDS_ITERATION              itHandle;



/* Create and initialize the search buffers that will be used. */

   Allocate the buffer that will hold the information about the object(s)

   found in the search. */

    cCode = NWDSAllocBuf(DEFAULT_MESSAGE_LEN, &foundObjectsBfr);&
    if(cCode)

    {

        freeSearchResources ( filterBuffer, foundObjectsBfr, attrNames);

        return(errorResponse( NULL, "getOptimalObjInfo" , "NWDSAllocBuf" ,

                                cCode , responseBuffer ));

    }



/* Allocate the buffer that will hold the attribute names for the search

    to return. */

    cCode = NWDSAllocBuf(DEFAULT_MESSAGE_LEN, &attrNames);&
    if(cCode)

    {

        freeSearchResources ( filterBuffer, foundObjectsBfr, attrNames);

        return(errorResponse( NULL, "getOptimalObjInfo" , "NWDSAllocBuf" ,

                    cCode , responseBuffer ));

    }



/* Initialize the buffer that will hold the attribute names for the

    search to return. */

    cCode = NWDSInitBuf(userCntxtHndl,  DSV_SEARCH, attrNames);

    if(cCode)

    {

        freeSearchResources ( filterBuffer, foundObjectsBfr, attrNames);

        return(errorResponse( NULL, "getOptimalObjInfo" , "NWDSInitBuf" ,

                    cCode , responseBuffer ));

    }



//  Allocate the buffer that will hold the search filter information

    cCode = NWDSAllocBuf(DEFAULT_MESSAGE_LEN, &filterBuffer);&
    if(cCode)

    {

        freeSearchResources ( filterBuffer, foundObjectsBfr, attrNames);

        return(errorResponse( NULL, "getOptimalObjInfo" , "NWDSAllocBuf" ,

                    cCode , responseBuffer ));

    }



//  Initialize the buffer that will hold the search filter information 

    cCode = NWDSInitBuf(userCntxtHndl, DSV_SEARCH_FILTER, filterBuffer);

    if(cCode)

    {

        freeSearchResources ( filterBuffer, foundObjectsBfr, attrNames);

        return(errorResponse( NULL, "getOptimalObjInfo" , "NWDSInitBuf" ,

                    cCode , responseBuffer ));

    }



/* Define the expression tree for the search and install attribute

    names into the attrNames buffer */

    if( defineSearch (userCntxtHndl, filterBuffer,responseBuffer)!= N_SUCCESS )

    {

        freeSearchResources ( filterBuffer, foundObjectsBfr, attrNames);

        return(N_FAILURE);

    }



//  Put "APIInfo Server" attribute name in the search attribute buffer */

    cCode = NWDSPutAttrName(    userCntxtHndl, attrNames, APIINFOSERVER);

    if(cCode)

    {

        freeSearchResources ( filterBuffer, foundObjectsBfr, attrNames);

        return(errorResponse( NULL, "getOptimalObjInfo" ,

                "NWDSPutAttrName" ,cCode , responseBuffer ));

    }





// Put "APIInfo Operator" attribute names in the search attribute buffer */

    cCode = NWDSPutAttrName(userCntxtHndl, attrNames, APIINFOOPERATOR);

    if(cCode)

    {

        freeSearchResources ( filterBuffer, foundObjectsBfr, attrNames);

        return(errorResponse( NULL, "getOptimalObjInfo" ,

               "NWDSPutAttrName" ,cCode  , responseBuffer));

    }



// Initialize the iteration handle. */

    itHandle = NO_MORE_ITERATIONS;



    do

    {

    // Perform the search.

        cCode = NWDSSearch( userCntxtHndl,

                        DS_ROOT_NAME,           // "[Root]"

                        DS_SEARCH_SUBTREE,  // 2

                        FALSE,

                        filterBuffer,

                        DS_ATTRIBUTE_VALUES,    // 0X01

                        FALSE,

                        attrNames,

                        &itHandle,&
                        NULL,

                        &countObjectsSearched,&
                        foundObjectsBfr );



        if (!cCode)

        {

        /* Retrieve info returned from search and select closest server */

            retValue =    selectOptimalObj(     userCntxtHndl,

                                serverDName,

                                operatorDName,

                                &bestResponseTime, &
                                foundObjectsBfr,

                                responseBuffer );

            switch(retValue)

            {

                case N_SUCCESS:

                {

                    if (!(strcmp(serverDName, "[Root]")) &&

                                    itHandle==NO_MORE_ITERATIONS )

                    {

                        strcpy( responseBuffer , "The selected tree's NDS schema

                            HAS been extended for API_Info. However, an API_Info

                            object has been found with an attribute containing the

                            [Root] distinguished name, therefore it is not fully

                            configured"");

                        return ( N_FAILURE );

                    }

                    break;

                }

                case CLOSE_ITERATION:

                {

                    if ( itHandle != NO_MORE_ITERATIONS )

                    {

                        NWDSCloseIteration(userCntxtHndl, itHandle, DSV_SEARCH);

                        itHandle = NO_MORE_ITERATIONS;

                    }

                    break;

                }

                default:

                {

                    if ( itHandle != NO_MORE_ITERATIONS )

                        NWDSCloseIteration( userCntxtHndl,itHandle, DSV_SEARCH);

                    freeSearchResources ( filterBuffer,foundObjectsBfr, attrNames);

                    return(N_FAILURE);

                }

            }

        }

        else

        {

            freeSearchResources ( filterBuffer, foundObjectsBfr,attrNames );

            return(errorResponse( NULL, "getOptimalObjInfo" ,

                         "NWDSSearch" ,cCode , responseBuffer ));

        }

     } while (itHandle != NO_MORE_ITERATIONS );



    freeSearchResources ( filterBuffer, foundObjectsBfr, attrNames);

    return( N_SUCCESS );

}  /* end of getOptimalObjInfo */





/************************** defineSearch *********************************

This function will define the search by breaking the search expression into a set 

of tokens and adding those tokens to the search filter.

***************************************************************************/

BOOL defineSearch   (   NWDSContextHandle   userCntxtHndl,

                pBuf_T          filterBuffer,

                char*           responseBuffer )

{

    NWDSCCODE           cCode;

    pFilter_Cursor_T        filterCursor=NULL;



   // Allocate space for search filter and check for errors. 

    cCode = NWDSAllocFilter(&filterCursor);&
    if(cCode)

    {

        if ( filterCursor )

            NWDSFreeFilter(filterCursor, NULL);

        return(errorResponse( NULL, "defineSearch" , "NWDSAllocFilter" ,

                                cCode , responseBuffer  ));

    }



//  Add filter tokens to define the search



//  Object Class

    cCode = NWDSAddFilterToken(filterCursor, FTOK_BASECLS, NULL, 0 );

    if (cCode)

    {

        if (filterCursor)

            NWDSFreeFilter(filterCursor, NULL);

        return(errorResponse( NULL, "defineSearch" , "NWDSAddFilterToken1"

                                 ,cCode , responseBuffer  ));

    }



//  APIInfoClass

    cCode =

    NWDSAddFilterToken( filterCursor, FTOK_ANAME,APIINFOCLASS, SYN_CI_STRING );

    if (cCode)

    {

        if ( filterCursor )

            NWDSFreeFilter( filterCursor, NULL );

        return(errorResponse( NULL, "defineSearch" , "NWDSAddFilterToken2"

                            ,cCode , responseBuffer  ));

    }



//  End Token

    cCode = NWDSAddFilterToken( filterCursor, FTOK_END, NULL, 0 );

    if (cCode)

    {

        if (filterCursor)

            NWDSFreeFilter(filterCursor, NULL);

        return( errorResponse( NULL, "defineSearch" , "NWDSAddFilterToken3" 

                            ,cCode , responseBuffer  ));

    }



//  Put the filter in the filter buffer.

    cCode = NWDSPutFilter(userCntxtHndl, filterBuffer, filterCursor, NULL);

    if(cCode)

    {

        return(errorResponse( NULL, "defineSearch" , "NWDSPutFilter" ,

                                cCode , responseBuffer  ));

  }

    return(N_SUCCESS);

} /* end of defineSearch */







/***************************** selectOptimalObj ************************

This function will take the buffer returned from the search, extract the object names, 

extract the host server from each object, and determine if it is the closest server so far.

****************************************************************************/

int selectOptimalObj(       NWDSContextHandle       userCntxtHndl,

                    char *              serverDName,

                    char *              operatorDName,

                    nuint *             bestResponseTime,

                    pBuf_T              foundObjectsBfr,

                    char *              responseBuffer )

{

    struct

    {

        char    attrName [MAX_DN_CHARS];

        char    attrVal [MAX_DN_CHARS];

    }   attrReadBfr [NUM_READ_ATTRS];   // one bfr per readable attribute



    NWCOUNT         objectCount,attrValCount,attrValCount;

    char            objectName[MAX_DN_CHARS],

                tempServerName[MAX_DN_CHARS],

                tempOpName[MAX_DN_CHARS];

    NWOBJECT_INFO   info;

    NWSYNTAX_ID syntaxID;

    int         objIndex,attrIndex;

    NWRCODE     retCode;

    NWDSCCODE   cCode;

    NWCONN_HANDLE   tconnHdl;

    nuint       time2Srvr;





/* Get the number of objects that were returned in the search. */

    cCode = NWDSGetObjectCount(userCntxtHndl, foundObjectsBfr, &objectCount);&
    if(cCode)

    {

        return(errorResponse( NULL, "selectOptimalObj" , 

                "NWDSGetObjectCount" ,cCode , responseBuffer  ));

    }



    if(objectCount == 0)

    {

        strcpy( responseBuffer , "The selected tree's NDS schema HAS been

            extended for API_Info.  However, no API_Info objects exist in

            the tree.  Contact your network administrator to create and

            configure an object.");

        return ( N_FAILURE );

    }





//  Get the name of the current object returned in the search.

    for (objIndex=0; objIndex < (int)objectCount; ++objIndex)

    {

        cCode = NWDSGetObjectName(userCntxtHndl, foundObjectsBfr, objectName,

                            &attrValCount, &info);&
        if(cCode)

        {

            return(errorResponse( NULL, "selectOptimalObj" , 

                "NWDSGetObjectName" ,cCode , responseBuffer  ));

        }

    /* Get the attribute names and values for the current object--we do not

        know which attribute we will read first */

        for(    attrIndex=0;

            attrIndex<(int)attrValCount <<attrIndex<NUM_READ_ATTRS;<
            attrIndex++ )

        {

            cCode = NWDSGetAttrName( userCntxtHndl,

                    foundObjectsBfr,

                    attrReadBfr[attrIndex].attrName,

                    &attrValCount,&
                    &syntaxID );&
            if(cCode)

            {

                return(errorResponse( NULL, "selectOptimalObj" , 

                    "NWDSGetAttrName" ,cCode , responseBuffer  ));

            }



            cCode = NWDSGetAttrVal( userCntxtHndl,foundObjectsBfr,syntaxID,

                            attrReadBfr[attrIndex].attrVal );

            if(cCode)

            {

                return(errorResponse( NULL, "selectOptimalObj" ,

                     "NWDSGetAttrVal" ,cCode , responseBuffer  ));

            }

        }

    /* Determine which attribute value references the Host Server attribute

        and which references the Operator.  Copies the appropriate values into

        temporary local variables. */

        if(strcmp ( "[Root]",attrReadBfr[0].attrVal ) &&

                strcmp ( "[Root]",attrReadBfr[1].attrVal ) )

        {

            if(!strcmp( attrReadBfr[0].attrName , APIINFOSERVER ))

            {

                strcpy( tempServerName , attrReadBfr[0].attrVal);

                strcpy( tempOpName , attrReadBfr[1].attrVal);

            }

            else

            {

                strcpy( tempOpName , attrReadBfr[0].attrVal);

                strcpy( tempServerName , attrReadBfr[1].attrVal);

            }



        /* Open a temporary connection to the server referenced by the current

            API_Info object. */

            cCode = NWDSOpenConnToNDSServer(        userCntxtHndl,

                                    tempServerName,

                                    &tconnHdl );&
            if (cCode)

            {

                return(errorResponse( NULL, "selectOptimalObj" ,

                "NWDSOpenConnToNDSServer" , cCode ,responseBuffer));

            }



        //  Determine the distance (in milliseconds) to the server

            retCode = NWCCGetConnInfo(  tconnHdl,

                                NWCC_INFO_DISTANCE,

                                sizeof(nuint),

                                &time2Srvr );&
            if (retCode)

            {

                return(errorResponse( NULL, "selectOptimalObj" , 

                "NWCCGetConnInfo",(int)retCode , responseBuffer));

            }



        //  Close the temporary connection

            retCode = NWCCCloseConn(tconnHdl);

            if (retCode)

            {

                return(errorResponse( NULL, "selectOptimalObj" , 

                "NWCCCloseConn" ,(int)retCode , responseBuffer  ));

            }



        /* If the distance (time) to the current server is closest so far,

            copy the time into bestResponseTime and copy the distinguished names

            for the server and operator into the appropriate variables. */

            if( time2Srvr < (*bestResponseTime) )

            {

                (*bestResponseTime) = time2Srvr;

                strcpy(serverDName,tempServerName);

                strcpy(operatorDName, tempOpName);

                if (*bestResponseTime < 1000)

                    return (CLOSE_ITERATION);

            }

        }

     }

    return((int)N_SUCCESS);

} /* end of selectOptimalObj */

Source for Handling the REQ_ALL_TREE_NMS Message

/* 

/ REQ_ALL_TREE_NMS is sent to dispatchGUIReq by API_GUI.obj when the user

/ presses "Tree Browse" button to obtain a list of all trees in the network

/ that the client is NOT currently connected to.  This message is handled by

/ the following API_Info client routines: getAllTreeNames , and some less

/ significant routines that can be found in the utilities section at the

/ end of this listing.

/*/#013;




/************************* getAllTreeNames ***************************

Puts a list of all the tree objects that can be seen on the default connection into the response buffer.

***************************************************************************/

NWDSCCODE   getAllTreeNames( char   * responseBuffer )

{

    NWCONN_HANDLE           prefConn=NULL;

    NWDSContextHandle       rootContext=NULL;

    NWDSCCODE               cCode;

    nint32                  scanIndex=-1;

    nstr8                       treeName[MAX_TREE_NAME_CHARS+1];



//  Get connection to preferred server, which will be used to obtain list

    cCode = NWGetPreferredDSServer( &prefConn );&
    if (cCode)

    {

        freeConnResources( prefConn, rootContext);

        return(errorResponse( NULL, "getAllTreeNames","NWGetPreferredDSServer",

                    cCode , responseBuffer));

    }



/* Create a context to be used in passing to the call to obtain the available

    trees. Set the context to the Root of the tree, because we do not need a

    user context for the call. */

    cCode = NWDSCreateContextHandle(&rootContext);&
    if (cCode)

    {

        freeConnResources( prefConn, rootContext);

        return(errorResponse( NULL, "getAllTreeNames","NWDSCreateContextHandle",

                    cCode , responseBuffer));

    }

    else

    {

        cCode = NWDSSetContext( rootContext, DCK_NAME_CONTEXT, "[Root]" );

        if(cCode)

        {

            freeConnResources( prefConn, rootContext);

            return(errorResponse( NULL, "getAllTreeNames" , "NWDSSetContext" ,

                    cCode , responseBuffer));

        }

    }



    do

    {

    /* Scan preferred server's SAP table to obtain all the available trees

        on the network. */

        cCode = NWDSScanForAvailableTrees(  rootContext,

                            prefConn,

                            "*",        // wild card character

                            &scanIndex,&
                            treeName );





        if( cCode == (NWDSCCODE)0x89FC  ) //if we can't find any more trees

        {

            cCode = N_SUCCESS;

            break;

        }

        else

            if( cCode )

            {

                errorResponse( NULL "getAllTreeNames","NWDSScanForAvailableTrees",

                            cCode , responseBuffer);

                break;

            }

            else

            {

                if(strlen(responseBuffer) <=(LIST_BUF_LNGTH- MAX_TREE_NAME_CHARS))

                {

            //  Add the current tree name to the responseBuffer

                    setTreeName(    responseBuffer,

                            treeName ,

                            DONT_MARK_PREFERRED ,

                            DONT_MARK_AUTHENTICATED );

                }

            }

    } while  (scanIndex != NO_MORE_ITERATIONS);



    freeConnResources( prefConn, rootContext);

    return(cCode);

} /* end of getAllTreeNames */

Source for Handling All NLM Service Request Messages

/* 

/ All requests not having anything to do with selecting a tree or setting up a

/ connection are sent to dispatchGUIReq by API_GUI.obj are assumed to be for 

/ the service NLM at the other end of the service connection (e.g., 

/ GET_RECORD, UPDATE_RECORD, etc.). This message is passed on through the 

/ service connection to the NLM by the makeNCPExtReq routine.

/*/#013;




/************************ makeNCPExtReq  **********************************

Calls NWNCPExtensionRequest to send buffers through service connection.

***************************************************************************/ void 

makeNCPExtReq (         char *  requestBuffer,

                WORD        reqBfrSize  ,

                char *      responseBuffer ,

                WORD *  respBfrSize)

{

    if ( NWNCPExtensionRequest (        gAPIServConn,

                        gNCPExtID,

                        requestBuffer,

                        reqBfrSize,

                        responseBuffer,

                        respBfrSize )== FAILURE )



    {   errorResponse( "The API you typed does not exist in the database.           

    "makeNCPExtReq" , "NWNCPExtensionRequest" , cCode,responseBuffer);

    }

} /* end of makeNCPExtReq */

Utilities Descriptions

/*  _\

/ The routines listed below are called to perform simple tasks by the

/ previously listed API_Info client routines.  Frequently, these routines 

/ have no NetWare calls.  To save space, only their descriptions are shown.

/*/#013;




freeAPIServConn - Frees connection resources allocated by program and closes the 

connection to the server which is running the NLM side of API_Info.  In addition to 

being called when an error code is returned in acquireAPIServConn,

this function is called when you exit API_Info.  (The source for this routine is 

listed later in this article's commentary).



freeSearchResources- Frees buffers allocated for the NDS search and closes the 

connection obtained from the primary connection reference which was used in the 

search.



freeUserContext- Frees context resources allocated by program.



freeAttrDefBuffers- Frees buffer resources allocated for the attribute definition 

search, also determines if current tree is extended for API_Info. 



freeConnResources - Frees resources allocated for preferred connection and root context.



errorResponse - Puts specified error info into the response buffer. After 

responseBuffer is returned to the GUI, the GUI will display error Info to user.



parseDistinguishedName - Simply parses up the distinguished name string into server name and 

context strings. No NetWare calls made in this routine.



filterDName - Simply filters typical NDS type identifiers out of a typeful name and 

eliminates the leading period in a distinguished name. 



deleteStr - Simply deletes the specified string from the targetStr.



getNameAndContext - Copies name and context from a distinguished name string into 

dest string buffers.



filterNWName - Simply filters specified trailing characters from a string.



setTreeName - Manages marking entries in the buffer so that the GUI can parse it 

properly (e.g., list of tree names with the client's preferred and selected trees 

marked appropriately).



markTreeName - Sets specified mark at specified location in treeList buffer.



checkTreeName - Finds specified tree name in treeList buffer and returns its status 

as the status currently recorded in the buffer by marks.



advanceCursor - Advances buffer cursor to position of next tree name.

Source Code Explanations

During the rest of this article, we will use the table in Figure 3 to help us anticipate what requests the GUI will send to the network code and when.

Figure 3: API_Info's NDS Operations Table.

Navigating NDS Trees in a Multi-Tree Environment

In response to user actions, API_GUI.OBJ passes messages to the routines listed earlier in this article to implement the API_Info client's NDS functionality. The following discussion explains how the source code handles the messages which are sent as a user attempts to connect to a new tree.

Note: Refer to the source for initNDSEnv for the following.

Handling the REQ_INIT Message

When the user launches the API_Info application, it immediately invokes dispatchGUIRequest to dispatch an initialize request to the initNDSEnv routine with the REQ_INIT message. initNDSEnv performs simple tasks like initializing unicode tables and getting locale information. If there have been no errors in initialization, the response buffer will be returned empty.

Note: Refer to the source for getConnectedTreeNames for the following.

Handling the REQ_CNCTD_TREE_NMS Message

Upon launch, after the REQ_INIT message, the GUI sends dispatchGUIRequest the REQ_CNCTD_TREE_NMS request causing the getConnectedTreeNames routine to be invoked. The first thing that getConnectedTreeNames does is to enter a loop which sequences through each entry in the client's connRef table to find out if its connection is authenticated and obtain its tree name.

The client's conref table contains references to each of the client's connections and the data associated with the connection, including the name of the connected station. The Connection Handle table contains indexes to the connection references in the Conref table.

Figure 4: A Client's Applications Can Reuse a Connection.

The ConnRef table enables a connection to a single server to be reused by many applications. In the example shown in Figure 4, the value of the connection handle awarded to the application is 3. However, the application is actually using the connection referenced by the first entry in the ConnRef table.

Figure 5: The NWCCConnInfo Data Structure.

Back in the getConnectedTreeNames routine, if we use the example shown in Figure 4, the first connRef value returned by NWCCScanConnRefs would be a 1. This connRef index would be sent to NWCCGetAllConnRefInfo along with a version specifier that should always be NWCC_INFO_VERSION, and a pointer to some local memory for an NWCCConnInfo data structure called connRefInfoBuf(shown in Figure 5).

The connRefInfoBufdata structure is used by NWCCGetAllConnRefInfo to return information about the connection specified by connRef. This loop is interested in the connection's authentication state and tree name. For the purposes of this discussion, we will assume that the server IncServer1 is on the NOVELL_INC tree.

setTreeName is a utility routine that is used to build tree-name lists in the response buffer. The connection's tree name is passed to setTreeName and is designated as a non-preferred tree so that setTreeName won't mark the tree name entry as the preferred tree.

Also sent to setTreeName is a boolean value indicating whether or not the new entry should be marked as authenticated. The expression for this parameter checks the authentication flag in connRefInfoBuf's authentication state member. If the connection is authenticated, the value sent will be TRUE.

In the next iteration, NWCCScanConnRefs obtains connRef 2, which references a connection to the IncServer2 server. NWCCGetAllConnRefInfo sets the connRefInfoBuf data structure to describe this connection. We will assume this server to also be on the NOVELL_INC tree. Since setTreeName does not allow redundant entries, NOVELL_INC remains the only entry in the list.

The next block of code in getConnectedTreeNames is intended to determine the name of the preferred tree and then mark its entry in the list as preferred. To do this, NWGetPreferredServer is called to get the connection handle to the preferred server.

NWCCGetConnInfo is a way to get specific information about an existing connection. In this case, we are specifiying that we would like the tree name for the connection. We also pass a local buffer to contain the tree name and the size of the buffer.

If there is a preferred tree name, getConnectedTreeNames calls setTreeName to find it in the list and add the preferred mark (an asterisk) to its entry.

The response buffer containing the list is then returned to the GUI, which parses it to obtain the client's preferred tree name. In this example, the preferred tree (in fact, the only tree) will be "NOVELL_INC."

Note: Refer to the source for dispatchGUIRequest for the following.

Handling a Failed REQ_CONNECT Request

Recall that the tree name, user distinguished name, password, and authFlag variables are defined in API_GUI.OBJ and are sent to dispatchGUIRequest only in the REQ_CONNECT message. Upon launch, after the REQ_INIT and the REQ_CNCTD_TREE_NMS requests have been handled, the GUI will send dispatchGUIRequest a REQ_CONNECT message along with thes parameters.

In our application launch example, this message is an attempt by API_Info to connect to the server specified by the preferred tree name that the GUI previously obtained with the REQ_CNCTD_TREE_NMS request. Since we are assuming that the client already has an authenticated connection to the preferred tree, the authFlag parameter would be TRUE, and the login information parameters would be ignored. We will also assume that the schema on the client's preferred tree, NOVELL_INC has not been extended for API_Info.

To handle the REQ_CONNECT request, dispatchGUIRequest invokes acquireAPIServConn. The purpose of acquireAPIServConn is to search the specified tree for the API_Info object with a Host Server attribute that references the closest server running an instance of the API_Info NLM.

Recall that a distinguished name consists of an object name together with a name context which describes the object and its location in the tree. The first call in acquireAPIServConn is to an application defined routine called parseDistinguishedName, which parses the fully distinguished userDName parameter into user name and name context strings. The client object's name context is used in the setupNDSEnv call to initialize a context that describes the location of the client's user object in the specified tree. The user name will be used later if a login operation is required.

You learned how to initialize an NDS context in the second article. One of the things that setupNDSEnv does is to initialize a context for the specified tree name. This context will be used throughout the task of acquiring an optimal service connection. By using this context as a parameter in all of the appropriate NDS calls, NDS will know what the target tree is.

setupNDSEnv also checks the schema on the specified tree for the existence of the API_Info host server and operator attributes. In our scenario, these two attributes don't exist on the NOVELL_INC tree, so setupNDSEnv puts an error string into the response buffer and returns an error. This error string is then passed back to the GUI, which displays it to the user in an error dialog (shown in Figure 6).

Figure 6: If the Preferred Tree is not extended for API_Info, an error will result.

Notice that the error string advises the user to click the Tree button (as shown in Figure 7) to select a tree whose schema has been extended for API_Info.

Figure 7: API_Info's Tree Button Causes a Request for all Connected Trees.

Before the GUI can display the Available Trees window, it sends the network code another request to obtain a list of all of the trees to which this client is connected, for display in its Available Trees window. We will assume that getConnectedTreeNames builds the list as it did before. Since the client is currently only connected to the NOVELL_INC tree, that is the only name in the list returned to the GUI (as shown in Figure 8).

Figure 8: API_Info's Available Trees Window Displays all Connected Trees.

Handling the REQ_ALL_TREE_NMS Request

To view a list of tree names that are not connected to the client, the user selects the Available Tree window's Browse button. In response to this action, the GUI sends a REQ_ALL_TREE_NMS request to the network code to obtain a list of all of the trees installed on the network. This request causes dispatchGUIRequest to invoke the getAllTreeNames routine.

Note: Refer to the source for getAllTreeNames for the following.

The NetWare client has both a preferred tree and a preferred server on that tree. The purpose of getAllTreeNames is to ask the preferred server to scan its SAP table for all tree name entries, remove redundant and already connected tree names, and return the resulting list in the response buffer.

getAllTreeNames first gets a connection handle to the preferred server and then allocates a context and sets its name to "[Root]."

Next, getAllTreeNames begins an iteration loop using the NWDSScanForAvailableTrees call. During this loop, the preferred server is interrogated using the new connection handle to obtain each tree name listed in its SAP table. You can filter the tree names found by NWDSScanForAvailableTrees by specifying wildcard characters in the scanFilter parameter. This example specifies a single asterisk to find all tree names.

Each tree name is returned in a locally allocated buffer called treeName and sent to setTreeName to be added to the list in the response buffer. After the list is built, the connection is closed and the context is freed.

The all trees list in the response buffer is returned to the GUI, which parses the list and displays it in the Tree Browser window (Figure 9). This allows the user the option to select a new tree and acquire an API_Info service connection to a server on that tree.

Figure 9: API_Info's Tree Browser allows users to select trees for new connections.

Note: Refer to Figure 10 for the following discussion.

Handling a REQ_DISCONNECT Request

Our user selects the DevServices tree because he knows that tree has been extended for API_Info. The GUI then adds DevServices as an unauthenticated tree to the connected trees list displayed in the Available Trees window (Figure 10).

Figure 10: API_Info's Acquire Connection Button initiates a search for an optimal service instance in the selected tree.

The user then clicks the Acquire Service Connection button. Since the client was not authenticated to the DevServices tree, the GUI asks the user for login information before making any requests to the network code. At this point, the user can cancel without making any requests.

After pressing the Acquire Service Connection button, if the user enters the login information and does not cancel, the GUI will send the network code a REQ_DISCONNECT, the first of two messages needed to acquire a connection to the client's optimal service instance on the new tree (Figure 3).

The REQ_DISCONNECT message directs the network code to close the connection handle currently being used by API_Info before obtaining a new connection handle. In response to this message, dispatchGUIRequest invokes the freeAPIServConn routine. As you can see in the source code below, freeAPIServConn simply calls NWCCCloseConn to free connection resources for the current connection if there is one.

/************************ freeAPIServConn  **********************************

Frees resources allocated by program and closes the connection to the server which is

running the NLM side of API_Info.  This function is also called when you exit API_Info.

***************************************************************************/ NWCCODE 

freeAPIServConn ()

{

    NWRCODE rCode;

    if( gAPIServConn )

    {

        rCode = NWCCCloseConn( gAPIServConn );

        gAPIServConn = NULL;

    }

    return( (NWCCODE)rCode );

} /* end of freeAPIServConn */

Searching an NDS Tree to Acquire the Optimal Service Instance for your Client

The following discussion explains two methods that can be used to search an NDS tree for all instances of objects of a given class. The first method, although intuitive, is shown to potentially require a great deal of effort by the administrator to initialize and maintain the application. The discussion then explains how the second method can be used to solve the problems listed for the first method.

Handling a Successful REQ_CONNECT Request

The second request resulting from the Acquire Service Connection button click is another REQ_CONNECT message that asks the network code to acquire an optimal service connection on the newly specified DevServices tree (recall that REQ_CONNECT was also sent at program launch).

In order to acquire an optimal service connection, the network code must search the DevServices tree for API_Info objects, find the object whose Host Server attribute references the closest server, and use the login information to connect and login to the tree through that server.

Note: Refer to the source for acquireAPIServConn for the following.

The DevServices tree name is sent to the setupNDSEnv routine. setupNDSEnv determines that the schema on the DevServices tree has been extended for API_Info so acquireAPIServConn advances to the getOptimalObjectInfo call.

Note: Refer to the source for getOptimalObjectInfo for the following.

The purpose of getOptimalObjectInfo is to search the tree specified in its context parameter for API_Info objects and to find which object's host server attribute references the closest instance of an API_Info NLM on the tree. The host server and operator values read from the selected API_Info object will be returned in the serverDName and operatorDName character strings.

An NDS search requires several buffers. One of these is a buffer to contain the names of the attributes to be read from all of the found objects during the search. getOptimalObjectInfo allocates a buffer for this purpose, initializes it for a search, and adds the names of the two attributes that will be read during the search, "APIInfo Server" and "APIInfo Operator."

getOptimalObjectInfo then allocates another buffer to contain the values that will be read from these attributes in the found objects. Since this buffer is the response buffer, it does not need to be initialized.

Next getOptimalObjectInfo allocates a buffer to contain a search expression describing the characteristics for the objects we want to find during the search. defineSearch is then called to build the search expression.

Note: Refer to the source for defineSearch for the following.

The first call in defineSearch allocates a search filter. The search expression will be built inside this filter, which will then be installed into filterBuf.

To help us understand what a search expression is, let's consider the example in Figure 11. A search implemented using this expression would yield a response buffer containing values from all objects with an object class attribute containing the value "user" with a phone number attribute containing a value equal to "555-3926."

Each of the boxes shown in the expression tree in Figure 11 is called a token. As you can see, some tokens contain values. The first attribute name token, for example, contains the value "Object Class" specifying the Object Class attribute.

The relational operator token FTOK_EQ contains no values but expresses a relationship between two tokens that do. Another example of a relational token would be less than or equal to.

Figure 11: A Search Expression is built with tokens.

The first subexpression is completed with an attribute value token type containing the value "user." Two subexpressions can be joined together with a logical operator token like FTOK_AND expressing a logical relationship between subexpressions or values. There are also tokens signifying parentheses, which can be used to group subexpressions or values.

The second subexpression is defined with tokens in the same way as the first.

Figure 12: An Intuitive but inefficient expression to find all instances of objects of the API_Info class.

Tokens are installed sequentially into a search filter with the NWDSAddFilterToken call. The search filter is then packaged in an NDS buffer.

Shown in the upper left of Figure 12 is an expression that one might intuitively use to find all API_Info objects in a tree. But it has a problem. To understand the problem, look at API_Info's special attribute and class definitions shown in Figure 12. Recall from the previous article that the APIInfo Server and Operator attributes were especially defined to possess the DS_PUBLIC_READ constraint.

This constraint is necessary in these attributes because without it anyone who needs to read the attributes must be added as a trustee for these attributes in the Access Control List of every API_Info object in the tree. Since reading these attributes is part of an API_Info client's search process for an optimal service, without the DS_PUBLIC_READ constraint, all API_Info users would have to be added as trustees to all API_Info objects in the tree. This might be a lot of work for the administrator.

Now, here is why this expression would necessitate all of this extra work for the administrator. Recall that the object class attribute is inherited from TOP and is the attribute which contains the class name value for the object. Notice that the object class attribute does not possess the DS_PUBLIC_READ constraint.

Since the object class attribute must be read for this expression to work, this means that every API_Info user must be added as a trustee for the object class attribute in every API_Info object's ACL attribute.

Adding trustees to an object's ACL is a task that is usually performed by an administrator using the NetWare Administrator application. If there are very many users and many API_Info objects, this task could be very labor intensive.

Fortunately, Novell realized that developers would want to be able to search for their own objects without this tremendous administrative overhead. So Novell invented a special token to facilitate this kind of search. The special base class token designates the search for object class values as an exception to the standard attribute value search mechanism. The new expression shown in Figure 13 consists of the base class token accompanied by an attribute name token with a value specifying API_Info's class name. This expression will find all API_Info objects in a tree without requiring users to be listed as trustees.

Figure 13: The base class token is the efficient way to find all instances of objects of a given class.

Recall that NWDSAddFilterToken (prototype shown in Figure 14), is used to add tokens to the search filter. The cursor parameter points to the next token location available in the filter. Its value is updated by NDS during the NWDSAddFilterToken call. The tok parameter should be set to the Novell defined symbol for the desired token.

Figure 14: The Prototype for IntraNetware's NWDSAddFilterToken routine.

If the token type requires a value such as would an attribute name token, that would be passed in the val parameter. Finally, if there is a value, NDS needs to know its syntax so that it can properly store the value in the search filter.

defineSearch's first call to NWDSAddFilterToken installs the base class token in the search filter. Subsequent calls to NWDSAddFilterToken install API_Info's class name and terminate the expression.

NWDSPutFilter installs the filter in the buffer previously allocated for it and control is returned to getOptimalObjInfo.

Note: Refer to the source for getOptimalObjectInfo for the following.

Now that the filterBuffer, attrNames, and foundObjectsBfr buffers have been prepared, they are passed to NDS in the NWDSSearch call. This NWDSSearch will find all objects whose class name is APIInfoClass and have their APIInfo Server and Operator attribute values read into foundObjectsBfr.

There may be several iterations of the NWDSSearch call depending on the number of API_Info objects in the tree. After each iteration of NWDSSearch, foundObjectsBfr is passed to selectOptimalObj to determine which of the found objects contains a reference to the closest service.

Note: Refer to the source for selectOptimalObj for the following.

The serverDName and operatorDName strings passed into selectOptimalObj will ultimately be used by acquireAPIServConn to establish a connection with the target server. The value in these strings will be reset with new distinguished names every time selectOptimalObj finds an object which references a closer service.

bestResponseTime is a variable declared in getOptimalObjInfo that is used to remember the distance from the closest API_Info server so far in the search. The distance value in bestResponseTime will be updated every time selectOptimalObj finds an object which references a closer service.

The objectsFoundBuffer contains the APIInfo Server and Operator values read from every object found during the current NWDSSearch iteration.

Shown in Figure 15 is a search results buffer containing data from two API_Info objects. Reading a search buffer is a very particular process. Objects and their attributes in an NDS search response buffer are referenced by the curPos buffer pointer in the Buf_T data structure.

Figure 15: curPos reflects the location of the next item in the buffer.

The curPos pointer is advanced through the buffer by special NDS routines that read the buffer. The routines have to be called in a particular order. All search buffer read routines begin with NWDSGetObjectCount (as shown in the pseudo-code example in Figure 16) to count the number of objects in the buffer.

Most search buffer read routines have three loops, as shown: one loop for the objects, one loop for the attributes within each object, and one loop for the values owned by each attribute type.

Figure 16: Routines to unpack search buffers generally have three nested loops.

The first call in the object loop is NWDSGetObjectName, which sets the buffer pointer to the next object in the buffer. NWDSGetObjectName also reads all of the object's information, including the number of attributes owned by the object. In this case there are two attributes.

The first call in the attribute loop is NWDSGetAttrName, which sets the buffer pointer to the next attribute in the buffer. NWDSGetAttrName also reads all of the attribute's information, including the number of values owned by the attribute. In this case there are two values. Note that the order of the object's attributes in the buffer is not known at this point.

The first call in the value's loop is NWDSGetAttrVal, which sets the buffer pointer to the next value in the buffer. NWDSGetAttrVal also reads all of the value's data.

A typical search buffer read routine will process or temporarily store the value's data at this point. It is important to note here that since the attributes can be in any order, the loop must use the name value read during the previous NWDSGetAttrName call to determine which attribute it is currently processing. This process continues until all values are read from the buffer.

Note: Refer to the source for selectOptimalObj for the following.

Back in selectOptimalObj, the call to NWDSGetObjectCount in selectOptimalObj gets the number of objects in foundObjectsBfr. This value is used to test the index in the loop below it which will iterate through each set of object attributes in the buffer.

NWDSGetObjectName sets the buffer pointer to the next object in the buffer, in this case the first one, and retrieves the object's name. NWDSGetObjectName also gets a count for the number of attributes read from the object. The count value is important when you are reading optional attributes because otherwise you would not know how many attributes were actually possessed by each object.

In our case, because we are reading two mandatory attributes only, as specified by the attributeNames buffer, the attribute count will always be two.

NWDSGetAttrName moves the buffer pointer to the next attribute, in this case the first attribute in the current object. NWDSGetAttrName also gets the name of the newly selected attribute from the buffer and puts it into the attribute name string of the first element in attrReadBfr. attrReadBfr is a structure array defined locally in this routine for temporary storage of the attribute's name and value.

At this point, we can't tell whether the attribute's name is "APIInfo Server" or "APIInfo Operator" since attributes can be read from the object in any order. By the way, MAX_DN_CHARS is defined by NetWare to be the maximum number of characters allowed in an ANSI distinguished name string.

NWDSGetAttrName will read the attribute to set attrValCount to the number of values in the buffer owned by the attribute. This value is handy if the attribute can have multiple values, but the API_Info attributes were defined to have single values. Since there can be only one value per attribute, this search buffer read routine does not need the values loop we discussed earlier.

Also returned is the attribute's syntax ID. In our case this will always be the distinguished name syntax since that is how we defined the APIInfo Server and Operator attributes when we extended the schema.

NWDSGetAttrVal moves the buffer pointer to the next value, in this case the first and only value for the attribute. NWDSGetAttrVal also puts the value in the attrVal member of the first element of the attrReadBfr array. This value will contain a distinguished name for either the APIInfo Server or Operator value depending on which one is being read. The subloop then reads the other attribute name and value.

This string compare is performed to make sure that the APIInfo Server distinguished name gets put into the tempServerName string and the APIInfo Operator distinguished name gets put into the tempOpName string.

NWDSOpenConnToNDSServer opens a connection to the server specified by the distinguished name in the tempServerName parameter. During the process of creating the connection, the NetWare client develops a data profile of the connection. The data structure shown in Figure 5 reveals the kind of connection information that is available from this profile. At this point, we are interested in the distance value.

NWCCGetConnInfo is a general purpose routine that can retrieve different kinds of information about a connection. In this case, we pass the NetWare defined NWCC_INFO_DISTANCE constant to have NWCCGetConnInfo obtain the value reflecting the client's distance from the server on the other end of the new connection. This value is returned in the time2Server parameter. After NWCCGetConnInfo returns, NWCCCloseConnection closes the temporary connection.

bestResponseTime was initialized to the maximum value for an integer in getOptimalObjInfo, so time2Server is guaranteed to be less than bestResponseTime. Because the current object's time2Server is the best response time so far, the bestResponseTime variable is set to time2Server and the current object's server and operator values are copied into serverDName and operatorDName.

The distance value in time2Server expresses distance in terms of transmission time. To keep this explanation short, we will assume that time2Server indicates that a transmission from this object's server would take less than a thousand milliseconds to arrive at our client. This is an acceptable distance, so selectOptimalObj returns CLOSE_ITERATION to getOptimalObjInfo.

Note: Refer to the source for getOptimalObjInfo for the following.

In getOptimalObjInfo, the search loop is terminated, the search buffers are freed, and control is returned to acquireAPIServConn.

Connecting and Authenticating to a Tree to Access a Service

Once the API_Info object whose server distinguished name attribute refers to the closest server is found in the selected tree, an authenticated and licensed connection must be built to the specified server before a service like NetWare's NCP extension can be accessed on it. The following discussion explains how this is done in the API_Info client's source code.

Note: Refer to the source for acquireAPIServConn for the following.

The NetWare Connection Model

In acquireAPIServConn, the distinguished name in the serverDName string returned by getOptimalObjInfo is used to establish a new connection to the target server. This is the server determined to be the closest server on the tree running the API_Info NLM.

The NetWare connection model has three connection states:


Connection State
Explanation

Connected but not authenticated

The station has an internetwork address for the station that it wants to communicate with but does not have the credentials necessary to use anyessential services, such as the NetWare file system.

Authenticated but not licensed

Each NetWare network has a limited number of licensed connections. This number is set at the time of purchase. Authenticated but not licensed meansthat the appropriate credential has been authenticated for the connection butNetWare has not yet awarded it with a licensed status. Without a licensedconnection, the client still cannot use any essential NetWare services.

Authenticated and licensed

This connection state allows the client full use of all essential NetWare services that the client's user has been given rights to.

The NWDSOpenConnToNDSServer call will allow the client to achieve the first connection state. Again, the handle returned by NWDSOpenConnToNDSServer may actually be referencing an existing connRef in the connRef table.

The authFlag parameter from the GUI would have been TRUE if the tree specified by treeName were already authenticated. If that were the case, acquireAPIServConn would use the NWDSAuthenticateConn call to perform a background authentication on the new connection handle and so achieve the second connection state.

To do this, NWDSAuthenticateConn would find the existing authenticated connection to the tree using its context parameter to get the tree's name for comparison with tree names of existing connRefs. NWDSAuthenticateConn would then associate the new connection handle with the credentials that already exist for the found authenticated connection to the tree.

However, because the authFlag is FALSE, the userDName and password parameters from the GUI are valid and a full login is required. In this case, NWDSLogin will be used to authenticate the connection.

Notice that NWDSLogin does not receive the connection handle returned by NWDSOpenConnToNDSServer, so how does it know what connection is being authenticated? It so happens that NWDSLogin uses the NetWare client's internal latest connection variable which it accesses using the client's current context. In our example, this will be the same connection that we just obtained with the NWDSOpenConnToNDSServer call.

After NWDSLogin, the connection is authenticated but it is still not licensed. An authenticated client is awarded a licensed connection when it makes a request for an essential NetWare service and a licensed connection is available. It turns out that NetWare's NCP extension facility is just such an essential NetWare service.

When NWGetNCPExtensionInfoByName communicates over the new connection to obtain the extension ID of the API_Info NLM on the target server, NetWare checks to see if this tree has any licensed connections available. If there is one available, the connection is fully licensed.

The unique NCP extension ID from the server will now enable API_Info to pass NCP transactions from our user on to the API_Info NLM running on the server. If NWGetNCPExtensionInfoByName fails, getOperatorInfo is called to obtain the phone number from the operator's object and display it to the user. (A source listing for getOperatorInfo is not included in this article.) Otherwise, the user is now free to use the functionality and data available from the NLM portion of API_Info.

Congratulations and Summary

If you have read all three of the articles in this series, not only have you demonstrated a lot of patience but you have also probably obtained a wholistic understanding of what it takes to develop a simple IntranetWare client service application like API_Info.

In the first article of this series, you learned how to develop a simple NLM which manages access to a small database of NetWare API prototypes using a NetWare Core Protocol extension.

In the second article, after you learned about the NDS schema, you learned how to write a snapin DLL, which could add two new attributes and a new API_Info object class to a tree's schema and enable the administrator to create and edit API_Info objects.

In this article, you learned how to develop a client which could use NDS to find each API_Info object on a tree and temporarily connect to the server referred to by its APIInfo Server attribute value. During each temporary connection, the network code determined the server's distance from the client. After processing all API_Info objects, the network code determined which one contained the reference to the closest server and established a permanent connection to that server. In this way, the closest NLM instance could be found.

I sincerely hope you will be able to transfer the information from this series into the skills that you will need to create your own projects.

Use the following web address to download API_Info's complete sources and collateral files (i.e., DLLS, data files, etc.): http://www.novell.com/research


Disclaimer

The origin of this information may be internal or external to Novell. While Novell makes all reasonable efforts to verify this information, Novell does not make explicit or implied claims to its validity.

© 2014 Novell